Browse Source

Merge pull request #15443 from thingsboard/port-openapi

Refactored APIs to meet OpenAPI standard
pull/15467/head
Viacheslav Klimov 1 month ago
committed by GitHub
parent
commit
1f7041359f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 81
      application/pom.xml
  2. 703
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  3. 4
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  4. 4
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  5. 259
      application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java
  6. 2
      application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java
  7. 46
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  8. 24
      application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java
  9. 15
      application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
  10. 4
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  11. 12
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  12. 162
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  13. 4
      application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
  14. 350
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  15. 24
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  16. 41
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  17. 28
      application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java
  18. 65
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  19. 30
      application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
  20. 28
      application/src/main/java/org/thingsboard/server/controller/DomainController.java
  21. 42
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  22. 15
      application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
  23. 168
      application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
  24. 46
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  25. 19
      application/src/main/java/org/thingsboard/server/controller/EventController.java
  26. 4
      application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
  27. 4
      application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java
  28. 6
      application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java
  29. 4
      application/src/main/java/org/thingsboard/server/controller/MobileAppController.java
  30. 41
      application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
  31. 4
      application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java
  32. 37
      application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
  33. 30
      application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
  34. 2
      application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java
  35. 8
      application/src/main/java/org/thingsboard/server/controller/QueueController.java
  36. 17
      application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java
  37. 16
      application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java
  38. 15
      application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
  39. 32
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  40. 40
      application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java
  41. 19
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  42. 110
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  43. 18
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  44. 15
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  45. 9
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java
  46. 6
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  47. 80
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  48. 30
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  49. 31
      application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
  50. 92
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java
  51. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java
  52. 7
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  53. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java
  54. 8
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
  55. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java
  56. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java
  57. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java
  58. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java
  59. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java
  60. 51
      application/src/main/resources/thingsboard-openapi.properties
  61. 178
      application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
  62. 168
      application/src/test/java/org/thingsboard/server/client/AIModelApiClientTest.java
  63. 127
      application/src/test/java/org/thingsboard/server/client/AbstractApiClientTest.java
  64. 99
      application/src/test/java/org/thingsboard/server/client/AdminApiClientTest.java
  65. 163
      application/src/test/java/org/thingsboard/server/client/AlarmApiClientTest.java
  66. 106
      application/src/test/java/org/thingsboard/server/client/AlarmCommentApiClientTest.java
  67. 117
      application/src/test/java/org/thingsboard/server/client/ApiKeyApiClientTest.java
  68. 84
      application/src/test/java/org/thingsboard/server/client/AssetApiClientTest.java
  69. 135
      application/src/test/java/org/thingsboard/server/client/AssetProfileApiClientTest.java
  70. 286
      application/src/test/java/org/thingsboard/server/client/CalculatedFieldApiClientTest.java
  71. 113
      application/src/test/java/org/thingsboard/server/client/CustomerApiClientTest.java
  72. 123
      application/src/test/java/org/thingsboard/server/client/DashboardApiClientTest.java
  73. 114
      application/src/test/java/org/thingsboard/server/client/DeviceApiClientTest.java
  74. 53
      application/src/test/java/org/thingsboard/server/client/DeviceConnectivityApiClientTest.java
  75. 157
      application/src/test/java/org/thingsboard/server/client/DeviceProfileApiClientTest.java
  76. 105
      application/src/test/java/org/thingsboard/server/client/DomainApiClientTest.java
  77. 141
      application/src/test/java/org/thingsboard/server/client/EdgeApiClientTest.java
  78. 289
      application/src/test/java/org/thingsboard/server/client/EntityQueryApiClientTest.java
  79. 182
      application/src/test/java/org/thingsboard/server/client/EntityRelationApiClientTest.java
  80. 267
      application/src/test/java/org/thingsboard/server/client/EntityViewApiClientTest.java
  81. 156
      application/src/test/java/org/thingsboard/server/client/MobileAppApiClientTest.java
  82. 278
      application/src/test/java/org/thingsboard/server/client/NotificationApiClientTest.java
  83. 138
      application/src/test/java/org/thingsboard/server/client/Oauth2ApiClientTest.java
  84. 261
      application/src/test/java/org/thingsboard/server/client/OtaPackageApiClientTest.java
  85. 72
      application/src/test/java/org/thingsboard/server/client/RpcV1ApiClientTest.java
  86. 133
      application/src/test/java/org/thingsboard/server/client/RpcV2ApiClientTest.java
  87. 163
      application/src/test/java/org/thingsboard/server/client/RuleChainApiClientTest.java
  88. 151
      application/src/test/java/org/thingsboard/server/client/TbImageApiClientTest.java
  89. 128
      application/src/test/java/org/thingsboard/server/client/TbResourceApiClientTest.java
  90. 150
      application/src/test/java/org/thingsboard/server/client/TelemetryApiClientTest.java
  91. 119
      application/src/test/java/org/thingsboard/server/client/TenantApiClientTest.java
  92. 179
      application/src/test/java/org/thingsboard/server/client/TenantProfileApiClientTest.java
  93. 78
      application/src/test/java/org/thingsboard/server/client/TwoFactorAuthApiClientTest.java
  94. 135
      application/src/test/java/org/thingsboard/server/client/UserApiClientTest.java
  95. 155
      application/src/test/java/org/thingsboard/server/client/WidgetTypeApiClientTest.java
  96. 5
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  97. 420
      application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java
  98. 1
      application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java
  99. 2
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java
  100. 3
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

81
application/pom.xml

@ -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,82 @@
</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-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<repositories>
<repository>
<id>jenkins</id>

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

@ -15,11 +15,15 @@
*/
package org.thingsboard.server.config;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
@ -31,6 +35,7 @@ import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.RequestBody;
@ -39,11 +44,12 @@ import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.tags.Tag;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.RouterOperationCustomizer;
import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer;
import org.springdoc.core.utils.SpringDocUtils;
import org.springdoc.core.models.GroupedOpenApi;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.properties.SwaggerUiConfigProperties;
@ -54,26 +60,35 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestParam;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse;
import org.thingsboard.server.exception.ThingsboardErrorResponse;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.auth.rest.LoginResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@ -83,16 +98,30 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@Profile("!test")
public class SwaggerConfiguration {
@PostConstruct
public void configureModelResolver() {
ModelResolver.enumsAsRef = true;
SpringDocUtils.getConfig().replaceWithSchema(ByteBuffer.class,
new Schema<String>().type("string").format("byte"));
}
public static final String LOGIN_ENDPOINT = "/api/auth/login";
public static final String REFRESH_TOKEN_ENDPOINT = "/api/auth/token";
private static final String LOGIN_PASSWORD_SCHEME = "HTTP login form";
private static final String API_KEY_SCHEME = "API key form";
private static final String LOGIN_PASSWORD_SCHEME = "HttpLoginForm";
private static final String API_KEY_SCHEME = "ApiKeyForm";
private static final ApiResponses loginResponses = loginResponses();
private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false);
private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true);
// Populated by mapAwareConverter, consumed by customOpenApiCustomizer.
// Keyed by the schema name that swagger-core generates (see resolveSchemaName).
private final Map<String, List<String>> schemaPropertyOrders = new ConcurrentHashMap<>();
private final Map<String, Set<String>> schemaOwnProps = new ConcurrentHashMap<>();
// Tracks schema name → fully-qualified class names to detect collisions.
private final Map<String, Set<String>> schemaNameToClasses = new ConcurrentHashMap<>();
@Value("${swagger.api_path:/api/**}")
private String apiPath;
@Value("${swagger.security_path_regex}")
@ -157,9 +186,9 @@ public class SwaggerConfiguration {
.in(SecurityScheme.In.HEADER)
.description("""
Enter the API key value with 'ApiKey' prefix in format: **ApiKey <your_api_key_value>**
Example: **ApiKey tb_5te51SkLRYpjGrujUGwqkjFvooWBlQpVe2An2Dr3w13wjfxDW**
<br>**NOTE**: Use only ONE authentication method at a time. If both are authorized, JWT auth takes the priority.<br>
""");
@ -213,11 +242,12 @@ public class SwaggerConfiguration {
private void addLoginOperation(OpenAPI openAPI) {
var operation = new Operation();
operation.summary("Login method to get user JWT token data");
operation.operationId("login");
operation.description("""
Login method used to authenticate user and get JWT token data.
Value of the response **token** field can be used as **X-Authorization** header value:
`X-Authorization: Bearer $JWT_TOKEN_VALUE`.""");
var requestBody = new RequestBody().description("Login request")
@ -235,11 +265,12 @@ public class SwaggerConfiguration {
private void addRefreshTokenOperation(OpenAPI openAPI) {
var operation = new Operation();
operation.summary("Refresh user JWT token data");
operation.operationId("refreshToken");
operation.description("""
Method to refresh JWT token. Provide a valid refresh token to get a new JWT token.
The response contains a new token that can be used for authorization.
`X-Authorization: Bearer $JWT_TOKEN_VALUE`""");
var requestBody = new RequestBody().description("Refresh token request")
@ -260,7 +291,6 @@ public class SwaggerConfiguration {
return GroupedOpenApi.builder()
.group(groupName)
.pathsToMatch(apiPath)
.addRouterOperationCustomizer(routerOperationCustomizer(localSpringDocParameterNameDiscoverer))
.addOperationCustomizer(operationCustomizer())
.addOpenApiCustomizer(customOpenApiCustomizer())
.build();
@ -270,9 +300,34 @@ public class SwaggerConfiguration {
@Lazy(false)
ModelConverter mapAwareConverter() {
return (type, context, chain) -> {
// Strip field-level @JsonIgnoreProperties from context annotations so it
// doesn't pollute the global schema. The OpenAPI schema should show all
// properties; field-level ignore is a serialization concern only.
Annotation[] ctxAnnotations = type.getCtxAnnotations();
if (ctxAnnotations != null) {
Annotation[] filtered = Arrays.stream(ctxAnnotations)
.filter(a -> !(a instanceof JsonIgnoreProperties))
.toArray(Annotation[]::new);
if (filtered.length != ctxAnnotations.length) {
type.ctxAnnotations(filtered);
}
}
JavaType javaType = Json.mapper().constructType(type.getType());
if (javaType != null) {
Class<?> cls = javaType.getRawClass();
Schema<?> atomicSchema = switch (cls.getName()) {
case "java.util.concurrent.atomic.AtomicInteger" -> new IntegerSchema().format("int32");
case "java.util.concurrent.atomic.AtomicLong" -> new IntegerSchema().format("int64");
case "com.google.common.util.concurrent.AtomicDouble" -> new IntegerSchema().format("double");
default -> null;
};
if (atomicSchema != null) {
return atomicSchema;
}
}
if (chain.hasNext()) {
Schema schema = chain.next().resolve(type, context, chain);
JavaType javaType = Json.mapper().constructType(type.getType());
if (javaType != null) {
Class<?> cls = javaType.getRawClass();
if (Map.class.isAssignableFrom(cls)) {
@ -282,6 +337,26 @@ public class SwaggerConfiguration {
schema.setProperties(null);
}
}
} else {
// Precompute property order and own-prop names for this class.
// The actual reordering happens later in the OpenApiCustomizer,
// which has access to the final state of all component schemas
// (including ones where the ModelConverter only sees a $ref).
try {
var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType);
String schemaName = resolveSchemaName(javaType);
Set<String> classes = schemaNameToClasses.computeIfAbsent(schemaName, k -> ConcurrentHashMap.newKeySet());
if (classes.add(cls.getName()) && classes.size() > 1) {
log.error("Duplicate OpenAPI schema name '{}' mapped by: {}. Use @Schema(name = ...) to disambiguate.", schemaName, classes);
}
schemaPropertyOrders.put(schemaName, resolvePropertyOrder(cls, beanDesc));
Set<String> ownProps = computeOwnPropNames(cls, beanDesc);
if (!ownProps.isEmpty()) {
schemaOwnProps.put(schemaName, ownProps);
}
} catch (Exception e) {
log.debug("Failed to resolve property order for {}: {}", cls.getName(), e.getMessage());
}
}
}
return schema;
@ -292,45 +367,19 @@ public class SwaggerConfiguration {
}
private void addDefaultSchemas(OpenAPI openAPI) {
var jsonNodeSchema = ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(JsonNode.class)).schema;
jsonNodeSchema.setType("any");
//noinspection unchecked
jsonNodeSchema.setExamples(List.of(JacksonUtil.newObjectNode()));
jsonNodeSchema.setDescription("A value representing the any type (object or primitive)");
Schema<?> errorCodeSchema = new Schema<>()
.type("integer")
.description("Platform error code")
._enum(Arrays.stream(ThingsboardErrorCode.values())
.map(ThingsboardErrorCode::getErrorCode)
.collect(Collectors.toList()));
openAPI.getComponents()
.addSchemas("JsonNode", jsonNodeSchema)
.addSchemas("LoginRequest", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(LoginRequest.class)).schema)
.addSchemas("LoginResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(LoginResponse.class)).schema)
.addSchemas("ThingsboardErrorResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardErrorResponse.class)).schema)
.addSchemas("ThingsboardCredentialsExpiredResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardCredentialsExpiredResponse.class)).schema);
}
private RouterOperationCustomizer routerOperationCustomizer(SpringDocParameterNameDiscoverer localSpringDocParameterNameDiscoverer) {
return (routerOperation, handlerMethod) -> {
String[] pNames = localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod());
String[] reflectionParametersNames = Arrays.stream(handlerMethod.getMethod().getParameters()).map(java.lang.reflect.Parameter::getName).toArray(String[]::new);
if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull)) {
pNames = reflectionParametersNames;
}
MethodParameter[] parameters = handlerMethod.getMethodParameters();
List<String> requestParams = new ArrayList<>();
for (var i = 0; i < parameters.length; i++) {
var methodParameter = parameters[i];
RequestParam requestParam = methodParameter.getParameterAnnotation(RequestParam.class);
if (requestParam != null) {
String pName = StringUtils.isNotBlank(requestParam.value()) ? requestParam.value() :
pNames[i];
if (StringUtils.isNotBlank(pName)) {
requestParams.add(pName);
}
}
}
if (!requestParams.isEmpty()) {
var path = routerOperation.getPath() + "{?" + String.join(",", requestParams) + "}";
routerOperation.setPath(path);
}
return routerOperation;
};
.addSchemas("ThingsboardCredentialsExpiredResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardCredentialsExpiredResponse.class)).schema)
.addSchemas("ThingsboardErrorCode", errorCodeSchema)
.addSchemas("AiChatModelConfig", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(AiChatModelConfig.class)).schema);
}
private OperationCustomizer operationCustomizer() {
@ -347,6 +396,19 @@ public class SwaggerConfiguration {
var apiKeyRequirement = createSecurityRequirement(API_KEY_SCHEME);
return openAPI -> {
// Fail fast on duplicate schema names — two different classes resolving to the same
// OpenAPI schema name causes one to silently overwrite the other.
List<String> duplicates = schemaNameToClasses.entrySet().stream()
.filter(e -> e.getValue().size() > 1)
.map(e -> "'" + e.getKey() + "' mapped by: " + e.getValue())
.sorted()
.toList();
if (!duplicates.isEmpty()) {
throw new IllegalStateException(
"Duplicate OpenAPI schema names detected. Use @Schema(name = ...) to disambiguate:\n "
+ String.join("\n ", duplicates));
}
var paths = openAPI.getPaths();
paths.entrySet().stream()
.peek(entry -> {
@ -370,17 +432,131 @@ public class SwaggerConfiguration {
});
sortedPaths.setExtensions(paths.getExtensions());
openAPI.setPaths(sortedPaths);
if (openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null) {
Map<String, Schema> schemas = openAPI.getComponents().getSchemas();
// Fix all schemas: if they have additionalProperties but no type, set type to object
schemas.forEach((schemaName, schema) -> {
if (schema.getAdditionalProperties() != null && schema.getType() == null) {
schema.setType("object");
log.debug("Added type 'object' to schema: {}", schemaName);
}
});
// Springdoc creates duplicate schemas with an "Object" suffix when a type is
// resolved through multiple inheritance paths or via generic type resolution.
// Remove the "*Object" duplicate when the base schema exists (either
// pre-registered in addDefaultSchemas or generated by springdoc).
for (String name : new ArrayList<>(schemas.keySet())) {
if (!name.endsWith("Object")) continue;
String baseName = name.substring(0, name.length() - "Object".length());
if (!schemas.containsKey(baseName)) continue;
schemas.remove(name);
String refToRemove = "#/components/schemas/" + name;
schemas.values().forEach(s -> {
if (s.getAllOf() != null) {
s.getAllOf().removeIf(allOfEntry -> refToRemove.equals(((Schema<?>) allOfEntry).get$ref()));
}
});
log.debug("Removed duplicate schema '{}' (base '{}' exists)", name, baseName);
}
// Remove duplicate or redundant inline entries in allOf. Springdoc can
// generate multiple inline property blocks when resolving a type through
// multiple parent paths (e.g. record + sealed interface). One block may be
// a strict subset of another (same properties, but the superset has extras
// like "modelType"). Keep only the superset in that case.
schemas.values().forEach(schema -> {
if (schema.getAllOf() != null && schema.getAllOf().size() > 1) {
List<Schema> allOf = schema.getAllOf();
Set<Integer> redundant = new HashSet<>();
for (int i = 0; i < allOf.size(); i++) {
if (redundant.contains(i)) continue;
Schema a = allOf.get(i);
if (a.get$ref() != null || a.getProperties() == null) continue;
for (int j = i + 1; j < allOf.size(); j++) {
if (redundant.contains(j)) continue;
Schema b = allOf.get(j);
if (b.get$ref() != null || b.getProperties() == null) continue;
if (a.getProperties().entrySet().containsAll(b.getProperties().entrySet())) {
redundant.add(j); // b is a subset of a
} else if (b.getProperties().entrySet().containsAll(a.getProperties().entrySet())) {
redundant.add(i); // a is a subset of b
break;
}
}
}
if (!redundant.isEmpty()) {
List<Schema> filtered = new ArrayList<>();
for (int i = 0; i < allOf.size(); i++) {
if (!redundant.contains(i)) {
filtered.add(allOf.get(i));
}
}
allOf.clear();
allOf.addAll(filtered);
}
}
});
// Fix polymorphic properties: replace inline oneOf with base type $ref
schemas.values().forEach(schema -> {
replaceInlineOneOfProperties(schema, schemas);
if (schema.getAllOf() != null) {
List<Schema> allOf = schema.getAllOf();
for (Schema allOfElement : allOf) {
replaceInlineOneOfProperties(allOfElement, schemas);
}
}
});
// Deduplicate allOf child schemas: remove properties that are already defined
// in the referenced parent schema to avoid duplication (e.g. EntityId children).
schemas.forEach((schemaName, schema) -> {
Set<String> ownProps = schemaOwnProps.getOrDefault(schemaName, Set.of());
deduplicateAllOfProperties(schema, schemas, ownProps);
});
// Reorder properties for all component schemas. This runs after all
// schemas are finalized so it covers schemas the ModelConverter only
// saw as a $ref (e.g. interface-based discriminator types like EntityId).
schemas.forEach((schemaName, schema) -> {
List<String> propOrder = schemaPropertyOrders.getOrDefault(schemaName, List.of());
reorderSchemaProperties(schema, propOrder);
});
// Fix polymorphic request/response bodies: replace inline oneOf with base type $ref
paths.values().stream()
.flatMap(pathItem -> pathItem.readOperationsMap().values().stream())
.forEach(operation -> {
// Request bodies
if (operation.getRequestBody() != null && operation.getRequestBody().getContent() != null) {
replaceInlineOneOfInContent(operation.getRequestBody().getContent(), schemas);
}
// Response bodies
if (operation.getResponses() != null) {
operation.getResponses().values().stream()
.filter(response -> response.getContent() != null)
.forEach(response -> replaceInlineOneOfInContent(response.getContent(), schemas));
}
});
}
// Set JsonNode schema last so model scanning cannot overwrite it
openAPI.getComponents().addSchemas("JsonNode", new Schema<>()
.description("A value representing the any type (object or primitive)")
.example(JacksonUtil.newObjectNode()));
var sortedSchemas = new TreeMap<>(openAPI.getComponents().getSchemas());
openAPI.getComponents().setSchemas(new LinkedHashMap<>(sortedSchemas));
};
}
private SecurityRequirement createSecurityRequirement(String schemeName) {
return new SecurityRequirement().addList(schemeName, Arrays.asList(
Authority.SYS_ADMIN.name(),
Authority.TENANT_ADMIN.name(),
Authority.CUSTOMER_USER.name()
));
return new SecurityRequirement().addList(schemeName, List.of());
}
private Tag extractTagFromPath(Map.Entry<String, PathItem> entry) {
@ -388,9 +564,134 @@ public class SwaggerConfiguration {
return tagName != null ? tagFromTagItem(tagName) : null;
}
private String findBaseTypeForOneOf(Map<String, Schema> schemas, List<Schema> oneOfSchemas) {
if (oneOfSchemas.isEmpty()) {
return null;
}
for (Schema oneOfSchema : oneOfSchemas) {
String ref = oneOfSchema.get$ref();
if (ref == null) {
continue;
}
String refName = ref.substring(ref.lastIndexOf('/') + 1);
// Check if this entry is itself a base type with discriminator
Schema<?> candidate = schemas.get(refName);
if (candidate != null && candidate.getDiscriminator() != null
&& candidate.getDiscriminator().getMapping() != null) {
return refName;
}
// Check if this subtype is in another schema's discriminator mapping
String baseType = schemas.entrySet().stream()
.filter(entry -> {
Schema<?> schema = entry.getValue();
if (schema.getDiscriminator() != null && schema.getDiscriminator().getMapping() != null) {
return schema.getDiscriminator().getMapping().values().stream()
.anyMatch(r -> r.endsWith("/" + refName));
}
return false;
})
.map(Map.Entry::getKey)
.findFirst()
.orElse(null);
if (baseType != null) {
return baseType;
}
// Check if other oneOf items extend this candidate via allOf (parent-child without discriminator)
if (candidate != null) {
boolean isParent = oneOfSchemas.stream()
.filter(s -> s.get$ref() != null && !s.get$ref().equals(ref))
.anyMatch(s -> {
String otherName = s.get$ref().substring(s.get$ref().lastIndexOf('/') + 1);
Schema<?> otherSchema = schemas.get(otherName);
return otherSchema != null && otherSchema.getAllOf() != null &&
otherSchema.getAllOf().stream().anyMatch(
a -> a.get$ref() != null && a.get$ref().endsWith("/" + refName));
});
if (isParent) {
return refName;
}
}
}
return null;
}
private void replaceInlineOneOfInContent(Content content, Map<String, Schema> schemas) {
content.values().forEach(mediaType -> {
Schema<?> schema = mediaType.getSchema();
if (schema != null && schema.getOneOf() != null && !schema.getOneOf().isEmpty()) {
String baseType = findBaseTypeForOneOf(schemas, schema.getOneOf());
if (baseType != null) {
Schema<?> refSchema = new Schema<>();
refSchema.set$ref("#/components/schemas/" + baseType);
mediaType.setSchema(refSchema);
log.debug("Replaced oneOf in content with $ref to {}", baseType);
}
}
});
}
@SuppressWarnings("unchecked")
private void replaceInlineOneOfProperties(Schema<?> schema, Map<String, Schema> allSchemas) {
if (schema == null || schema.getProperties() == null) {
return;
}
schema.getProperties().forEach((propName, propSchema) -> {
if (propSchema instanceof Schema) {
Schema<?> prop = (Schema<?>) propSchema;
// Check if additionalProperties has oneOf (polymorphic map values)
if (prop.getAdditionalProperties() instanceof Schema) {
Schema<?> additionalProps = (Schema<?>) prop.getAdditionalProperties();
if (additionalProps.getOneOf() != null && !additionalProps.getOneOf().isEmpty()) {
String baseType = findBaseTypeForOneOf(allSchemas, additionalProps.getOneOf());
if (baseType != null) {
Schema<?> refSchema = new Schema<>();
refSchema.set$ref("#/components/schemas/" + baseType);
prop.setAdditionalProperties(refSchema);
log.debug("Replaced oneOf in additionalProperties with $ref to {} in property {}", baseType, propName);
}
}
// Check if additionalProperties is an array whose items has oneOf (e.g. Map<K, List<PolymorphicType>>)
if (additionalProps.getItems() != null && additionalProps.getItems().getOneOf() != null && !additionalProps.getItems().getOneOf().isEmpty()) {
String baseType = findBaseTypeForOneOf(allSchemas, additionalProps.getItems().getOneOf());
if (baseType != null) {
Schema<?> refSchema = new Schema<>();
refSchema.set$ref("#/components/schemas/" + baseType);
additionalProps.setItems(refSchema);
log.debug("Replaced oneOf in additionalProperties.items with $ref to {} in property {}", baseType, propName);
}
}
}
// If property has oneOf, try to find the base discriminated type
if (prop.getOneOf() != null && !prop.getOneOf().isEmpty()) {
String baseType = findBaseTypeForOneOf(allSchemas, prop.getOneOf());
if (baseType != null) {
Schema<?> refSchema = new Schema<>();
refSchema.set$ref("#/components/schemas/" + baseType);
if (prop.getDescription() != null) {
refSchema.setDescription(prop.getDescription());
}
if (prop.getReadOnly() != null) {
refSchema.setReadOnly(prop.getReadOnly());
}
schema.getProperties().put(propName, refSchema);
log.debug("Replaced oneOf with $ref to {} in property {}", baseType, propName);
}
}
}
});
}
private String tagItemFromPathItem(PathItem item) {
var operations = item.readOperationsMap().values();
var operation = operations.stream().findAny();
var operation = operations.stream().findFirst();
if (operation.isPresent()) {
var tags = operation.get().getTags();
if (tags != null && !tags.isEmpty()) {
@ -480,28 +781,34 @@ public class SwaggerConfiguration {
private static ApiResponses loginErrorResponses() {
ApiResponses apiResponses = new ApiResponses();
apiResponses.addApiResponse("401", errorResponse("Unauthorized",
Map.of(
"bad-credentials", errorExample("Bad credentials",
ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
"token-expired", errorExample("JWT token expired",
ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)),
"account-disabled", errorExample("Disabled account",
ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
"account-locked", errorExample("Locked account",
ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
"authentication-failed", errorExample("General authentication error",
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))
)
));
var credentialsExpiredSchema = new Schema<ThingsboardCredentialsExpiredResponse>().$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse");
apiResponses.addApiResponse("401 ", errorResponse("Unauthorized (**Expired credentials**)",
Map.of(
"credentials-expired", errorExample("Expired credentials",
ThingsboardCredentialsExpiredResponse.of("User password expired!", StringUtils.randomAlphanumeric(30)))
),
credentialsExpiredSchema
Map<String, Example> unauthorizedExamples = new LinkedHashMap<>();
unauthorizedExamples.put("bad-credentials", errorExample("Bad credentials",
ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)));
unauthorizedExamples.put("token-expired", errorExample("JWT token expired",
ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)));
unauthorizedExamples.put("account-disabled", errorExample("Disabled account",
ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)));
unauthorizedExamples.put("account-locked", errorExample("Locked account",
ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)));
unauthorizedExamples.put("authentication-failed", errorExample("General authentication error",
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)));
unauthorizedExamples.put("credentials-expired", errorExample("Expired credentials",
ThingsboardCredentialsExpiredResponse.of("User password expired!", "udgDQOpS1Q4ZFEL8qHF9s8cSKQ7d1h")));
Schema<? extends ThingsboardErrorResponse> unauthorizedSchema = new Schema<>();
unauthorizedSchema.oneOf(List.of(
new Schema<ThingsboardErrorResponse>().$ref("#/components/schemas/ThingsboardErrorResponse"),
new Schema<ThingsboardCredentialsExpiredResponse>().$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse")
));
apiResponses.addApiResponse("401", errorResponse("Unauthorized", unauthorizedExamples, unauthorizedSchema));
return apiResponses;
}
@ -520,10 +827,250 @@ public class SwaggerConfiguration {
return new ApiResponse().description(description).content(content);
}
/**
* Recursively collects all property names reachable from {@code schemaName}, walking the
* ancestor chain through allOf $ref entries (to handle multi-level inheritance).
* {@code visited} prevents infinite loops in case of circular references.
*/
@SuppressWarnings("unchecked")
private void collectAllProperties(String schemaName, Map<String, Schema> allSchemas,
Set<String> result, Set<String> visited) {
if (!visited.add(schemaName)) {
return;
}
Schema<?> schema = allSchemas.get(schemaName);
if (schema == null) {
return;
}
if (schema.getProperties() != null) {
result.addAll(schema.getProperties().keySet());
}
if (schema.getAllOf() != null) {
for (Schema<?> allOfElement : schema.getAllOf()) {
String ref = allOfElement.get$ref();
if (ref != null) {
String refName = ref.substring(ref.lastIndexOf('/') + 1);
collectAllProperties(refName, allSchemas, result, visited);
} else if (allOfElement.getProperties() != null) {
result.addAll(allOfElement.getProperties().keySet());
}
}
}
}
@SuppressWarnings("unchecked")
private void deduplicateAllOfProperties(Schema<?> schema, Map<String, Schema> allSchemas, Set<String> ownProps) {
if (schema.getAllOf() == null) {
return;
}
// Collect properties defined in any $ref'd parent within the allOf, recursively
// walking the ancestor chain (each parent may itself use allOf to extend a grandparent).
Set<String> parentProperties = new LinkedHashSet<>();
for (Schema<?> allOfElement : schema.getAllOf()) {
String ref = allOfElement.get$ref();
if (ref != null) {
String refName = ref.substring(ref.lastIndexOf('/') + 1);
collectAllProperties(refName, allSchemas, parentProperties, new LinkedHashSet<>());
}
}
if (parentProperties.isEmpty()) {
return;
}
// Properties to strip: in parent schema AND not declared as own-class fields.
// This removes inherited properties (from superclasses or pure interface getters)
// while keeping properties the class declares as its own fields.
Set<String> toStrip = new LinkedHashSet<>(parentProperties);
toStrip.removeAll(ownProps);
if (toStrip.isEmpty()) {
return;
}
// Strip from inline (non-$ref) allOf elements
schema.getAllOf().removeIf(allOfElement -> {
if (allOfElement.get$ref() != null) {
return false;
}
if (allOfElement.getProperties() != null) {
Map<String, Schema> filtered = new LinkedHashMap<>(allOfElement.getProperties());
filtered.keySet().removeAll(toStrip);
allOfElement.setProperties(filtered.isEmpty() ? null : filtered);
}
return allOfElement.getProperties() == null
&& allOfElement.getRequired() == null
&& allOfElement.getType() == null;
});
// Remove stripped properties from the schema's required list
if (schema.getRequired() != null) {
List<String> required = new ArrayList<>(schema.getRequired());
required.removeAll(toStrip);
schema.setRequired(required.isEmpty() ? null : required);
}
}
/**
* Computes the schema name that swagger-core will use for the given JavaType.
* For simple types, this is just the class simple name (e.g. {@code Device}).
* For parameterized types, type parameter names are appended
* (e.g. {@code PageData<Device>} becomes {@code PageDataDevice}).
* This matches the naming convention used by swagger-core's {@code TypeNameResolver}.
*/
private static String resolveSchemaName(JavaType javaType) {
Class<?> cls = javaType.getRawClass();
io.swagger.v3.oas.annotations.media.Schema schemaAnnotation =
cls.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class);
if (schemaAnnotation != null && !schemaAnnotation.name().isEmpty()) {
return schemaAnnotation.name();
}
StringBuilder sb = new StringBuilder(cls.getSimpleName());
if (javaType.hasGenericTypes()) {
for (int i = 0; i < javaType.containedTypeCount(); i++) {
JavaType param = javaType.containedType(i);
if (param != null) {
sb.append(param.getRawClass().getSimpleName());
}
}
}
return sb.toString();
}
/**
* Returns the JSON property names that are backed by fields declared directly in {@code cls}
* (not inherited from a superclass). Used to distinguish "own" from "inherited" properties
* when deduplicating allOf inline elements.
*/
private static Set<String> computeOwnPropNames(Class<?> cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) {
Map<String, String> allFieldToJson = new LinkedHashMap<>();
for (var prop : beanDesc.findProperties()) {
if (prop.getField() != null && prop.couldSerialize()) {
allFieldToJson.put(prop.getField().getName(), prop.getName());
}
}
Set<String> own = new LinkedHashSet<>();
for (Field f : cls.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
String jsonName = allFieldToJson.get(f.getName());
if (jsonName != null) own.add(jsonName);
}
return own;
}
@SuppressWarnings("unchecked")
private static void reorderSchemaProperties(Schema<?> schema, List<String> propOrder) {
if (schema.getProperties() != null && schema.getProperties().size() > 1) {
schema.setProperties(reorderProperties(schema.getProperties(), propOrder));
}
if (schema.getAllOf() != null) {
for (Schema<?> allOfElement : schema.getAllOf()) {
if (allOfElement.get$ref() != null) continue;
if (allOfElement.getProperties() != null && allOfElement.getProperties().size() > 1) {
allOfElement.setProperties(reorderProperties(allOfElement.getProperties(), propOrder));
}
}
}
}
private static LinkedHashMap<String, Schema> reorderProperties(Map<String, Schema> current, List<String> propOrder) {
var reordered = new LinkedHashMap<String, Schema>();
for (String name : propOrder) {
Schema prop = current.get(name);
if (prop != null) reordered.put(name, prop);
}
// Any properties not covered by propOrder are appended
// alphabetically to guarantee a deterministic stable order.
new TreeMap<>(current).forEach((k, v) -> reordered.putIfAbsent(k, v));
return reordered;
}
/**
* Resolves the property ordering for a schema class.
*
* <p>Returns a list of JSON property names in the order they should appear in the
* OpenAPI schema. The caller uses this list to reorder the schema's property map;
* any properties <b>not</b> present in the returned list are appended alphabetically
* by the caller's {@code TreeMap} fallback, guaranteeing a stable, deterministic order.
*
* <p><b>Resolution strategy (first match wins):</b>
* <ol>
* <li>If {@code @JsonPropertyOrder} with an explicit {@code value()} is found on the
* class or any interface in its ancestry, that list is returned as-is. Note: if the
* annotation lists only a subset of fields, those fields are ordered first and the
* remaining properties fall through to the caller's alphabetical fallback consistent
* with Jackson's own behaviour for partial {@code @JsonPropertyOrder}.</li>
* <li>Otherwise, field-backed properties are returned in declaration order (superclass
* fields first). Getter-only properties are intentionally excluded to avoid
* non-deterministic ordering across restarts.</li>
* </ol>
*/
private static List<String> resolvePropertyOrder(Class<?> cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) {
// If an explicit @JsonPropertyOrder is present on the class or any interface in its
// ancestry, honour it directly. Walk up the class hierarchy; for each class also walk
// the full interface hierarchy (including super-interfaces) via BFS.
for (Class<?> c = cls; c != null && c != Object.class; c = c.getSuperclass()) {
JsonPropertyOrder propOrder = c.getAnnotation(JsonPropertyOrder.class);
if (propOrder != null && !propOrder.alphabetic() && propOrder.value().length > 0) {
return Arrays.asList(propOrder.value());
}
Deque<Class<?>> ifaceQueue = new ArrayDeque<>(Arrays.asList(c.getInterfaces()));
Set<Class<?>> visitedIfaces = new LinkedHashSet<>();
while (!ifaceQueue.isEmpty()) {
Class<?> iface = ifaceQueue.poll();
if (!visitedIfaces.add(iface)) continue;
propOrder = iface.getAnnotation(JsonPropertyOrder.class);
if (propOrder != null && !propOrder.alphabetic() && propOrder.value().length > 0) {
return Arrays.asList(propOrder.value());
}
ifaceQueue.addAll(Arrays.asList(iface.getInterfaces()));
}
}
// Map backing field names to their JSON property names (respects @JsonProperty)
Map<String, String> fieldToJsonName = new LinkedHashMap<>();
for (var prop : beanDesc.findProperties()) {
if (prop.couldSerialize()) {
if (prop.getField() != null) {
fieldToJsonName.put(prop.getField().getName(), prop.getName());
} else {
// For transient fields, Jackson may not associate the field with the property.
// Fall back to using the property name as the field name key.
fieldToJsonName.putIfAbsent(prop.getName(), prop.getName());
}
}
}
// Walk class hierarchy (superclass first) to get field declaration order
List<Class<?>> hierarchy = new ArrayList<>();
for (Class<?> c = cls; c != null && c != Object.class; c = c.getSuperclass()) {
hierarchy.add(0, c);
}
List<String> ordered = new ArrayList<>();
for (Class<?> c : hierarchy) {
for (Field f : c.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
String jsonName = fieldToJsonName.get(f.getName());
if (jsonName != null) ordered.add(jsonName);
}
}
// Return only field-backed properties in declaration order.
// Getter-only properties (no backing field) are intentionally excluded: their set can vary
// between restarts (e.g. Optional-typed getters depend on Jackson module registration order),
// so including them here would make their position non-deterministic when some are in orderedNames
// and others are only in the schema map. The converter's TreeMap fallback handles ALL
// non-field-backed properties together in one alphabetical pass, guaranteeing stable order.
return ordered;
}
private static Example errorExample(String summary, ThingsboardErrorResponse example) {
var node = (ObjectNode) JacksonUtil.valueToTree(example);
node.put("timestamp", 1609459200000L);
return new Example()
.summary(summary)
.value(example);
.value(node);
}
}
}

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);

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);
}
}

15
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;
@ -74,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,
@ -107,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,
@ -141,8 +138,7 @@ public class AuditLogController extends BaseController {
"For example to see when a device was created, updated, assigned to some customer, or even deleted from the system. " +
PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@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,
@ -176,8 +172,7 @@ public class AuditLogController extends BaseController {
notes = "Returns a page of audit logs related to all entities in the scope of the current user's Tenant. " +
PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@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) {

12
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);
}

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)

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 {

15
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;
@ -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);
}
}

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);
}

7
application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java

@ -64,7 +64,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
private final RateLimitService rateLimitService;
private final TbLogEntityActionService logEntityActionService;
protected static final List<EntityType> SUPPORTED_ENTITY_TYPES = List.of(
public static final List<EntityType> SUPPORTED_ENTITY_TYPES = List.of(
EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE,
EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET,
EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE, EntityType.DEVICE,
@ -131,7 +131,10 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
@Override
public Comparator<EntityType> getEntityTypeComparatorForImport() {
return Comparator.comparing(SUPPORTED_ENTITY_TYPES::indexOf);
return Comparator.comparingInt(type -> {
int index = SUPPORTED_ENTITY_TYPES.indexOf(type);
return index >= 0 ? index : Integer.MAX_VALUE;
});
}
@SuppressWarnings("unchecked")

4
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java

@ -42,10 +42,6 @@ public abstract class BaseEntityExportService<I extends EntityId, E extends Expo
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, E mainEntity, D exportData) {
}
protected D newExportData() {
return (D) new EntityExportData<E>();
}
public abstract Set<EntityType> getSupportedEntityTypes();
protected void replaceUuidsRecursively(EntitiesExportCtx<?> ctx, JsonNode node, Set<String> skippedRootFields, Pattern includedFieldsPattern) {

8
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java

@ -70,7 +70,8 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
@Override
public final D getExportData(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException {
D exportData = newExportData();
@SuppressWarnings("unchecked")
D exportData = (D) EntityExportData.newInstance(entityId.getEntityType());
E entity = exportableEntitiesService.findEntityByTenantIdAndId(ctx.getTenantId(), entityId);
if (entity == null) {
@ -78,7 +79,6 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
}
exportData.setEntity(entity);
exportData.setEntityType(entityId.getEntityType());
setAdditionalExportData(ctx, entity, exportData);
if (entity instanceof HasVersion hasVersion) {
hasVersion.setVersion(null);
@ -223,8 +223,4 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
return internalUuid;
}
protected D newExportData() {
return (D) new EntityExportData<E>();
}
}

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java

@ -48,11 +48,6 @@ public class DeviceExportService extends BaseEntityExportService<DeviceId, Devic
}
}
@Override
protected DeviceExportData newExportData() {
return new DeviceExportData();
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.DEVICE);

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java

@ -36,11 +36,6 @@ public class OtaPackageExportService extends BaseEntityExportService<OtaPackageI
otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId()));
}
@Override
protected OtaPackageExportData newExportData() {
return new OtaPackageExportData();
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.OTA_PACKAGE);

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java

@ -61,11 +61,6 @@ public class RuleChainExportService extends BaseEntityExportService<RuleChainId,
}
}
@Override
protected RuleChainExportData newExportData() {
return new RuleChainExportData();
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.RULE_CHAIN);

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java

@ -38,11 +38,6 @@ public class WidgetTypeExportService extends BaseEntityExportService<WidgetTypeI
}
}
@Override
protected WidgetTypeExportData newExportData() {
return new WidgetTypeExportData();
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.WIDGET_TYPE);

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java

@ -45,11 +45,6 @@ public class WidgetsBundleExportService extends BaseEntityExportService<WidgetsB
exportData.setFqns(fqns);
}
@Override
protected WidgetsBundleExportData newExportData() {
return new WidgetsBundleExportData();
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.WIDGETS_BUNDLE);

51
application/src/main/resources/thingsboard-openapi.properties

@ -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.
#
# Lightweight startup for OpenAPI spec generation.
# Activated only via: --spring.profiles.active=openapi
spring.main.banner-mode=off
# Testcontainers PostgreSQL
database.ts.type=sql
database.ts_latest.type=sql
spring.datasource.url=jdbc:tc:postgresql:16.6:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.hikari.maximumPoolSize=5
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.properties.hibernate.order_by.default_null_ordering=last
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=none
# Disable transports
transport.http.enabled=false
transport.mqtt.enabled=false
transport.coap.enabled=false
transport.lwm2m.enabled=false
transport.snmp.enabled=false
coap.server.enabled=false
# Disable edges, integrations, EDQS
edges.enabled=false
integrations.rpc.enabled=false
service.integrations.supported=NONE
transport.gateway.dashboard.sync.enabled=false
queue.edqs.sync.enabled=false
queue.edqs.api.supported=false
usage.stats.report.enabled=false
service.type=monolith

178
application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java

@ -48,8 +48,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.predic
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate.StringOperation;
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule;
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.SpecificTimeSchedule;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
@ -128,32 +127,32 @@ public class AlarmRulesTest extends AbstractControllerTest {
);
Condition clearRule = new Condition("return temperature <= 25;", null, null);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, clearRule);
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
postTelemetry(deviceId, "{\"temperature\":100}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isSeverityUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
postTelemetry(deviceId, "{\"temperature\":101}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
postTelemetry(deviceId, "{\"temperature\":20}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCleared()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK);
@ -185,11 +184,11 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition(simpleExpression, null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":100}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -209,11 +208,11 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"values\": {\"temperature\": 50}, \"ts\": " + (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30) + "}"));
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -236,15 +235,15 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
for (int i = 0; i < 4; i++) {
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(10);
}
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -255,12 +254,12 @@ public class AlarmRulesTest extends AbstractControllerTest {
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(10);
}
checkAlarmResult(calculatedField, alarmResult -> alarmResult.getConditionRepeats() == 9, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> alarmResult.getConditionRepeats() == 9, alarmResult -> {
assertThat(alarmResult.isUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
});
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isSeverityUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -288,14 +287,14 @@ public class AlarmRulesTest extends AbstractControllerTest {
new AlarmConditionValue<>(null, "eventsCount"), null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"eventsCount\":" + eventsCount + "}");
for (int i = 0; i < eventsCount; i++) {
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(10);
}
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -319,13 +318,13 @@ public class AlarmRulesTest extends AbstractControllerTest {
);
Condition clearRule = new Condition("return powerConsumption < 3000;", null, clearDurationMs);
CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 5 seconds",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 5 seconds",
arguments, createRules, clearRule);
postTelemetry(deviceId, "{\"powerConsumption\":3500}");
Thread.sleep(createDurationMs - 2000);
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -334,9 +333,9 @@ public class AlarmRulesTest extends AbstractControllerTest {
postTelemetry(deviceId, "{\"powerConsumption\":2000}");
Thread.sleep(clearDurationMs - 2000);
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCleared()).isTrue();
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK);
assertThat(alarmResult.getConditionDuration()).isBetween(clearDurationMs, clearDurationMs + 2000);
@ -363,12 +362,12 @@ public class AlarmRulesTest extends AbstractControllerTest {
new AlarmConditionValue<Long>(null, "duration"), null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 2 seconds",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 2 seconds",
arguments, createRules, null);
postTelemetry(deviceId, "{\"powerConsumption\":3500}");
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"duration\":" + createDurationMs + "}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -391,10 +390,10 @@ public class AlarmRulesTest extends AbstractControllerTest {
new AlarmConditionValue<Long>(2000L, null), null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 2 seconds",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 2 seconds",
arguments, createRules, null);
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -424,12 +423,12 @@ public class AlarmRulesTest extends AbstractControllerTest {
device.setCustomerId(customerId);
device = doPost("/api/device", device, Device.class);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":50}");
postTelemetry(deviceId, "{\"temperature\":51}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -462,21 +461,21 @@ public class AlarmRulesTest extends AbstractControllerTest {
"location", StringOperation.NOT_CONTAINS, new AlarmConditionValue<>(null, "locationFilter")
), null, null);
CalculatedField calculatedField = createAlarmCf(customerId, "New resident",
AlarmRuleDefinition alarmRule = createAlarmRule(customerId, "New resident",
arguments, createRules, clearRule);
loginSysAdmin();
postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"locationFilter\":\"Kyiv\"}");
loginTenantAdmin();
postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Kyiv\"}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Lviv\"}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCleared()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK);
@ -501,7 +500,7 @@ public class AlarmRulesTest extends AbstractControllerTest {
new AlarmConditionValue<>(null, "schedule"))
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
String schedule = """
{"timezone":"Europe/Kiev","items":[{"enabled":false,"dayOfWeek":1,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":2,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":3,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":4,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":5,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":6,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":7,"startsOn":0,"endsOn":0}]}
@ -510,14 +509,14 @@ public class AlarmRulesTest extends AbstractControllerTest {
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(1000);
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
schedule = schedule.replace("\"enabled\":false", "\"enabled\":true");
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}");
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
// checking multiple debug events due to scheduled reevaluation (which also produces debug events)
CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream()
CalculatedFieldDebugEvent debugEvent = getDebugEvents(alarmRule.getId(), 5).stream()
.filter(event -> event.getResult() != null)
.findFirst().orElse(null);
assertThat(debugEvent).isNotNull();
@ -565,18 +564,18 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition(criticalExpression, null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "No Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "No Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isSeverityUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -596,19 +595,19 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
calculatedField.setName("New alarm type");
calculatedField = saveCalculatedField(calculatedField);
checkAlarmResult(calculatedField, alarmResult -> {
alarmRule.setName("New alarm type");
alarmRule = saveAlarmRule(alarmRule);
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -628,18 +627,18 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 100;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(1000);
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration();
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration();
((TbelAlarmConditionExpression) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition().getExpression())
.setExpression("return temperature >= 50;");
calculatedField = saveCalculatedField(calculatedField);
checkAlarmResult(calculatedField, alarmResult -> {
alarmRule = saveAlarmRule(alarmRule);
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -662,13 +661,13 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
for (int i = 0; i < eventsCountMajor; i++) {
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(10);
}
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -676,18 +675,18 @@ public class AlarmRulesTest extends AbstractControllerTest {
});
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
assertThat(alarmResult.getConditionRepeats()).isEqualTo(6);
});
// decreasing required events count for critical rule
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration();
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration();
((RepeatingAlarmCondition) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition())
.setCount(new AlarmConditionValue<>(6, null));
calculatedField = saveCalculatedField(calculatedField);
checkAlarmResult(calculatedField, alarmResult -> {
alarmRule = saveAlarmRule(alarmRule);
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isSeverityUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -719,20 +718,20 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= temperatureThreshold;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(1000);
// not created because tenant's threshold 100 is used
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getArguments().get("temperatureThreshold")
((AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration()).getArguments().get("temperatureThreshold")
.setRefDynamicSourceConfiguration(null);
// using threshold 50 on device level
calculatedField = saveCalculatedField(calculatedField);
alarmRule = saveAlarmRule(alarmRule);
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -755,7 +754,7 @@ public class AlarmRulesTest extends AbstractControllerTest {
Map<AlarmSeverity, Condition> createRules = Map.of(
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50 && humidity >= 50;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature and Humidity Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature and Humidity Alarm",
arguments, createRules, null, configuration -> {
configuration.getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails(
"temperature is ${temperature}, humidity is ${humidity}"
@ -765,18 +764,18 @@ public class AlarmRulesTest extends AbstractControllerTest {
postTelemetry(deviceId, "{\"temperature\":50}");
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"humidity\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getDetails().get("data").asText())
.isEqualTo("temperature is 50, humidity is 50");
});
((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails(
((AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails(
"UPDATED temperature is ${temperature}, humidity is ${humidity}"
);
calculatedField = saveCalculatedField(calculatedField);
checkAlarmResult(calculatedField, alarmResult -> {
alarmRule = saveAlarmRule(alarmRule);
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isFalse();
assertThat(alarmResult.isUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
@ -805,16 +804,16 @@ public class AlarmRulesTest extends AbstractControllerTest {
new AlarmConditionValue<>(schedule, null))
);
CalculatedField calculatedField = createAlarmCf(deviceId, "Illegal parking alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "Illegal parking alarm",
arguments, createRules, null);
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"parkingSpotOccupied\":true}");
Thread.sleep(10000);
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream()
CalculatedFieldDebugEvent debugEvent = getDebugEvents(alarmRule.getId(), 5).stream()
.filter(event -> event.getResult() != null)
.findFirst().orElse(null);
assertThat(debugEvent).isNotNull();
@ -838,11 +837,11 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
Alarm alarm = checkAlarmResult(calculatedField, alarmResult -> {
Alarm alarm = checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -851,7 +850,7 @@ public class AlarmRulesTest extends AbstractControllerTest {
doPost("/api/alarm/" + alarm.getId() + "/clear", AlarmInfo.class);
Thread.sleep(1000);
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.getAlarm().getId()).isNotEqualTo(alarm.getId());
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
@ -861,21 +860,21 @@ public class AlarmRulesTest extends AbstractControllerTest {
// TODO: MSA tests
private TbAlarmResult checkAlarmResult(CalculatedField calculatedField, Consumer<TbAlarmResult> assertion) {
return checkAlarmResult(calculatedField, null, assertion);
private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer<TbAlarmResult> assertion) {
return checkAlarmResult(alarmRule, null, assertion);
}
private TbAlarmResult checkAlarmResult(CalculatedField calculatedField,
private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule,
Predicate<TbAlarmResult> waitFor,
Consumer<TbAlarmResult> assertion) {
TbAlarmResult alarmResult = await().atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getLatestAlarmResult(calculatedField.getId()), result ->
.until(() -> getLatestAlarmResult(alarmRule.getId()), result ->
result != null && (waitFor == null || waitFor.test(result)));
assertion.accept(alarmResult);
Alarm alarm = alarmResult.getAlarm();
assertThat(alarm.getOriginator()).isEqualTo(originatorId);
assertThat(alarm.getType()).isEqualTo(calculatedField.getName());
assertThat(alarm.getType()).isEqualTo(alarmRule.getName());
return alarmResult;
}
@ -895,38 +894,37 @@ public class AlarmRulesTest extends AbstractControllerTest {
return JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class);
}
private CalculatedField createAlarmCf(EntityId entityId,
String alarmType,
Map<String, Argument> arguments,
Map<AlarmSeverity, Condition> createConditions,
Condition clearCondition,
Consumer<AlarmCalculatedFieldConfiguration>... modifier) {
private AlarmRuleDefinition createAlarmRule(EntityId entityId,
String alarmType,
Map<String, Argument> arguments,
Map<AlarmSeverity, Condition> createConditions,
Condition clearCondition,
Consumer<AlarmCalculatedFieldConfiguration>... modifier) {
Map<AlarmSeverity, AlarmRule> createRules = new HashMap<>();
createConditions.forEach((severity, condition) -> {
createRules.put(severity, toAlarmRule(condition));
});
AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null;
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setName(alarmType);
calculatedField.setType(CalculatedFieldType.ALARM);
AlarmRuleDefinition alarmRule = new AlarmRuleDefinition();
alarmRule.setEntityId(entityId);
alarmRule.setName(alarmType);
AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration();
configuration.setArguments(arguments);
configuration.setCreateRules(createRules);
configuration.setClearRule(clearRule);
calculatedField.setConfiguration(configuration);
calculatedField.setDebugSettings(DebugSettings.all());
alarmRule.setConfiguration(configuration);
alarmRule.setDebugSettings(DebugSettings.all());
if (modifier.length > 0) {
modifier[0].accept(configuration);
}
CalculatedField savedCalculatedField = saveCalculatedField(calculatedField);
AlarmRuleDefinition savedAlarmRule = saveAlarmRule(alarmRule);
CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getDebugEvents(savedCalculatedField.getId(), 1),
.until(() -> getDebugEvents(savedAlarmRule.getId(), 1),
events -> !events.isEmpty()).get(0);
latestEventId = debugEvent.getId();
return savedCalculatedField;
return savedAlarmRule;
}
private AlarmRule toAlarmRule(Condition condition) {

168
application/src/test/java/org/thingsboard/server/client/AIModelApiClientTest.java

@ -0,0 +1,168 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.AiModel;
import org.thingsboard.client.model.OpenAiChatModelConfig;
import org.thingsboard.client.model.OpenAiProviderConfig;
import org.thingsboard.client.model.PageDataAiModel;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class AIModelApiClientTest extends AbstractApiClientTest {
private static final String AI_PREFIX = "AiTest_";
@Test
public void testSaveAndGetAiModel() throws Exception {
long ts = System.currentTimeMillis();
String name = AI_PREFIX + "save_" + ts;
AiModel model = buildAiModel(name, "gpt-4o", 0.7);
AiModel saved = client.saveAiModel(model);
assertNotNull(saved);
assertNotNull(saved.getId());
assertEquals(name, saved.getName());
assertNotNull(saved.getConfiguration());
// get by id
AiModel fetched = client.getAiModelById(saved.getId().getId());
assertNotNull(fetched);
assertEquals(name, fetched.getName());
assertEquals(saved.getId().getId(), fetched.getId().getId());
}
@Test
public void testGetAiModelById() throws Exception {
long ts = System.currentTimeMillis();
AiModel saved = createAiModel("getbyid_" + ts);
AiModel fetched = client.getAiModelById(saved.getId().getId());
assertNotNull(fetched);
assertEquals(saved.getName(), fetched.getName());
assertEquals(saved.getId().getId(), fetched.getId().getId());
}
@Test
public void testUpdateAiModel() throws Exception {
long ts = System.currentTimeMillis();
AiModel saved = createAiModel("update_" + ts);
saved.setName(AI_PREFIX + "updated_" + ts);
OpenAiChatModelConfig updatedConfig = new OpenAiChatModelConfig();
updatedConfig.setModelId("gpt-4o-mini");
updatedConfig.setTemperature(0.3);
updatedConfig.setMaxOutputTokens(2048);
updatedConfig.setMaxRetries(50);
OpenAiProviderConfig providerConfig = new OpenAiProviderConfig();
providerConfig.setApiKey("test-api-key");
providerConfig.setBaseUrl("https://api.openai.com/v1");
updatedConfig.setProviderConfig(providerConfig);
updatedConfig.setProvider("OPENAI");
saved.setConfiguration(updatedConfig);
AiModel updated = client.saveAiModel(saved);
assertNotNull(updated);
assertEquals(saved.getId().getId(), updated.getId().getId());
assertEquals(AI_PREFIX + "updated_" + ts, updated.getName());
}
@Test
public void testDeleteAiModel() throws Exception {
long ts = System.currentTimeMillis();
AiModel saved = createAiModel("delete_" + ts);
UUID modelId = saved.getId().getId();
client.getAiModelById(modelId);
Boolean deleted = client.deleteAiModelById(modelId);
assertTrue(deleted);
assertReturns404(() -> client.getAiModelById(modelId));
}
@Test
public void testGetAiModels() throws Exception {
long ts = System.currentTimeMillis();
for (int i = 0; i < 3; i++) {
createAiModel("list_" + ts + "_" + i);
}
PageDataAiModel page = client.getAiModels(100, 0, AI_PREFIX + "list_" + ts, null, null);
assertNotNull(page);
assertEquals(3, page.getTotalElements().intValue());
for (AiModel m : page.getData()) {
assertTrue(m.getName().startsWith(AI_PREFIX + "list_" + ts));
}
}
@Test
public void testGetAiModelById_notFound() {
UUID nonExistentId = UUID.randomUUID();
assertReturns404(() -> client.getAiModelById(nonExistentId));
}
@Test
public void testGetAiModelsPagination() throws Exception {
long ts = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
createAiModel("paged_" + ts + "_" + i);
}
PageDataAiModel page1 = client.getAiModels(2, 0, AI_PREFIX + "paged_" + ts, null, null);
assertNotNull(page1);
assertEquals(5, page1.getTotalElements().intValue());
assertEquals(3, page1.getTotalPages().intValue());
assertEquals(2, page1.getData().size());
assertTrue(page1.getHasNext());
PageDataAiModel lastPage = client.getAiModels(2, 2, AI_PREFIX + "paged_" + ts, null, null);
assertEquals(1, lastPage.getData().size());
assertFalse(lastPage.getHasNext());
}
private AiModel buildAiModel(String name, String modelId, double temperature) {
OpenAiChatModelConfig config = new OpenAiChatModelConfig();
config.setModelId(modelId);
config.setTemperature(temperature);
config.setMaxRetries(50);
OpenAiProviderConfig openAiProviderConfig = new OpenAiProviderConfig();
openAiProviderConfig.setApiKey("test-api-key");
openAiProviderConfig.setBaseUrl("https://api.openai.com/v1");
config.setProviderConfig(openAiProviderConfig);
config.setProvider("OPENAI");
AiModel model = new AiModel();
model.setName(name);
model.setConfiguration(config);
return model;
}
private AiModel createAiModel(String suffix) throws Exception {
return client.saveAiModel(buildAiModel(AI_PREFIX + suffix, "gpt-4o", 0.7));
}
}

127
application/src/test/java/org/thingsboard/server/client/AbstractApiClientTest.java

@ -0,0 +1,127 @@
/**
* 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.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.thingsboard.client.ApiException;
import org.thingsboard.client.ThingsboardClient;
import org.thingsboard.client.model.ActivateUserRequest;
import org.thingsboard.client.model.Authority;
import org.thingsboard.client.model.JwtPair;
import org.thingsboard.client.model.User;
import org.thingsboard.client.model.UserId;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.controller.AbstractControllerTest;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@Slf4j
public abstract class AbstractApiClientTest extends AbstractControllerTest {
protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
protected static final ObjectMapper MAPPER = new ObjectMapper();
protected static final String TEST_PREFIX = "ApiClientTestDevice_";
protected static final String TEST_PREFIX_2 = "ApiClientTestDevice2_";
protected static final String CUSTOMER_USERNAME = "javaClientCustomer@thingsboard.org";
protected static final String TENANT_ADMIN_USERNAME = "javaClientTenant@thingsboard.org";
protected static final String TEST_PASSWORD = "password123";
protected ThingsboardClient client;
// FQN for Tenant/Customer to avoid collision with AbstractWebTest fields
protected org.thingsboard.client.model.Tenant savedClientTenant;
protected User clientTenantAdmin;
protected org.thingsboard.client.model.Customer savedClientCustomer;
protected User savedClientCustomerUser;
@Before
public void setUpJavaClient() throws Exception {
client = ThingsboardClient.builder()
.url("http://localhost:" + wsPort)
.build();
client.login("sysadmin@thingsboard.org", "sysadmin");
org.thingsboard.client.model.Tenant tenant = new org.thingsboard.client.model.Tenant();
tenant.setTitle("Java client test tenant");
savedClientTenant = client.saveTenant(tenant);
clientTenantAdmin = new User();
clientTenantAdmin.setAuthority(Authority.TENANT_ADMIN);
clientTenantAdmin.setTenantId(savedClientTenant.getId());
clientTenantAdmin.setEmail(TENANT_ADMIN_USERNAME);
clientTenantAdmin = client.saveUser(clientTenantAdmin, "false");
activateUserAndAuthorize(clientTenantAdmin);
org.thingsboard.client.model.Customer customer = new org.thingsboard.client.model.Customer();
customer.setTitle("Java client test customer");
customer.setTenantId(savedClientTenant.getId());
savedClientCustomer = client.saveCustomer(customer, null, null, null);
User customerUser = new User();
customerUser.setAuthority(Authority.CUSTOMER_USER);
customerUser.setTenantId(savedClientTenant.getId());
customerUser.setCustomerId(savedClientCustomer.getId());
customerUser.setEmail(CUSTOMER_USERNAME);
savedClientCustomerUser = client.saveUser(customerUser, "false");
activateUser(savedClientCustomerUser.getId(), "password123", false);
}
@After
public void tearDownJavaClient() {
client.login("sysadmin@thingsboard.org", "sysadmin");
client.deleteTenant(savedClientTenant.getId().getId().toString());
}
protected String getBaseUrl() {
return "http://localhost:" + wsPort;
}
protected void activateUserAndAuthorize(User user) throws ApiException {
JwtPair jwtPair = activateUser(user.getId(), TEST_PASSWORD, false);
client.setToken(jwtPair.getToken());
}
protected JwtPair activateUser(UserId userId, String password, boolean sendActivationMail) throws ApiException {
ActivateUserRequest activateRequest = new ActivateUserRequest();
activateRequest.setActivateToken(getActivateToken(userId));
activateRequest.setPassword(password);
return client.activateUser(activateRequest, sendActivationMail);
}
protected String getActivateToken(UserId userId) throws ApiException {
String activateTokenRegex = "/api/noauth/activate?activateToken=";
String activationLink = client.getActivationLink(userId.getId().toString());
return activationLink.substring(activationLink.lastIndexOf(activateTokenRegex) + activateTokenRegex.length());
}
protected void assertReturns404(ThrowingRunnable operation) {
try {
operation.run();
fail("Expected ApiException with 404 status code");
} catch (ApiException exception) {
assertEquals("Expected 404 status code but got " + exception.getCode(),
404, exception.getCode());
} catch (Exception e) {
fail("Expected ApiException but got " + e.getClass().getName() + ": " + e.getMessage());
}
}
}

99
application/src/test/java/org/thingsboard/server/client/AdminApiClientTest.java

@ -0,0 +1,99 @@
/**
* 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.client;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Test;
import org.thingsboard.client.model.AdminSettings;
import org.thingsboard.client.model.FeaturesInfo;
import org.thingsboard.client.model.JwtSettings;
import org.thingsboard.client.model.SecuritySettings;
import org.thingsboard.client.model.SystemInfo;
import org.thingsboard.client.model.UpdateMessage;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class AdminApiClientTest extends AbstractApiClientTest {
@Test
public void testAdminSettingsLifecycle() throws Exception {
// authenticate as sysadmin for admin settings management
client.login("sysadmin@thingsboard.org", "sysadmin");
// get mail settings
AdminSettings mailSettings = client.getAdminSettings("mail");
assertNotNull(mailSettings);
assertNotNull(mailSettings.getKey());
assertEquals("mail", mailSettings.getKey());
assertNotNull(mailSettings.getJsonValue());
// get general settings
AdminSettings generalSettings = client.getAdminSettings("general");
assertNotNull(generalSettings);
assertEquals("general", generalSettings.getKey());
assertNotNull(generalSettings.getJsonValue());
assertNotNull(generalSettings.getJsonValue().get("baseUrl").asText());
// update general settings and restore
((ObjectNode) generalSettings.getJsonValue()).put("prohibitDifferentUrl", true);
AdminSettings updatedGeneralSettings = client.saveAdminSettings(generalSettings);
assertTrue(updatedGeneralSettings.getJsonValue().get("prohibitDifferentUrl").asBoolean());
// get security settings
SecuritySettings securitySettings = client.getSecuritySettings();
assertNotNull(securitySettings);
assertNotNull(securitySettings.getPasswordPolicy());
Integer originalMaxAttempts = securitySettings.getMaxFailedLoginAttempts();
// update security settings
securitySettings.setMaxFailedLoginAttempts(10);
SecuritySettings updatedSecurity = client.saveSecuritySettings(securitySettings);
assertNotNull(updatedSecurity);
assertEquals(10, updatedSecurity.getMaxFailedLoginAttempts().intValue());
// restore original security settings
updatedSecurity.setMaxFailedLoginAttempts(originalMaxAttempts);
client.saveSecuritySettings(updatedSecurity);
// get JWT settings
JwtSettings jwtSettings = client.getJwtSettings();
assertNotNull(jwtSettings);
assertNotNull(jwtSettings.getTokenExpirationTime());
assertNotNull(jwtSettings.getRefreshTokenExpTime());
assertEquals("thingsboard.io", jwtSettings.getTokenIssuer());
assertNotNull(jwtSettings.getTokenSigningKey());
// get system info
SystemInfo systemInfo = client.getSystemInfo();
assertNotNull(systemInfo);
// get features info
FeaturesInfo featuresInfo = client.getFeaturesInfo();
assertNotNull(featuresInfo);
assertFalse(featuresInfo.getSmsEnabled());
assertFalse(featuresInfo.getOauthEnabled());
// check updates
UpdateMessage updateMessage = client.checkUpdates();
assertNotNull(updateMessage);
}
}

163
application/src/test/java/org/thingsboard/server/client/AlarmApiClientTest.java

@ -0,0 +1,163 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.Alarm;
import org.thingsboard.client.model.AlarmInfo;
import org.thingsboard.client.model.AlarmSeverity;
import org.thingsboard.client.model.AlarmStatus;
import org.thingsboard.client.model.Device;
import org.thingsboard.client.model.EntitySubtype;
import org.thingsboard.client.model.EntityType;
import org.thingsboard.client.model.PageDataAlarmInfo;
import org.thingsboard.client.model.PageDataEntitySubtype;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class AlarmApiClientTest extends AbstractApiClientTest {
@Test
public void testAlarmLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<Alarm> createdAlarms = new ArrayList<>();
// First, create devices to attach alarms to
Device device1 = new Device();
device1.setName("Device_For_Alarm_" + timestamp + "_1");
device1.setType("default");
Device createdDevice1 = client.saveDevice(device1, null, null, null, null);
Device device2 = new Device();
device2.setName("Device_For_Alarm_" + timestamp + "_2");
device2.setType("thermostat");
Device createdDevice2 = client.saveDevice(device2, null, null, null, null);
// Create 2 alarms (1 for each device)
for (int i = 0; i < 2; i++) {
Alarm alarm = new Alarm();
alarm.setType(((i % 2 == 0) ? "Temperature Alarm" : "Connection Alarm"));
alarm.setSeverity(((i % 2 == 0) ? AlarmSeverity.CRITICAL : AlarmSeverity.WARNING));
alarm.setOriginator((i % 2 == 0) ? createdDevice1.getId() : createdDevice2.getId());
Alarm createdAlarm = client.saveAlarm(alarm);
assertNotNull(createdAlarm);
assertNotNull(createdAlarm.getId());
assertEquals(alarm.getType(), createdAlarm.getType());
assertEquals(alarm.getSeverity(), createdAlarm.getSeverity());
createdAlarms.add(createdAlarm);
}
// Get all alarms
PageDataAlarmInfo allAlarms = client.getAllAlarms(100, 0, null, null, null, null, null, null, null, null, null);
assertNotNull(allAlarms);
assertNotNull(allAlarms.getData());
int initialSize = allAlarms.getData().size();
assertEquals("Expected at least 2 alarms, but got " + initialSize, 2, initialSize);
// Get alarms by entity (device1)
PageDataAlarmInfo device1Alarms = client.getAlarmsV2(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), 100, 0, null, null, null, null, null, null, null, null, null);
assertNotNull(device1Alarms);
assertEquals("Expected 1 alarms for device1", 1, device1Alarms.getData().size());
// Get alarm by id
Alarm searchAlarm = createdAlarms.get(0);
Alarm fetchedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString());
assertEquals(searchAlarm.getType(), fetchedAlarm.getType());
assertEquals(searchAlarm.getSeverity(), fetchedAlarm.getSeverity());
// Get alarm info
AlarmInfo alarmInfo = client.getAlarmInfoById(searchAlarm.getId().getId().toString());
assertNotNull(alarmInfo);
assertEquals(searchAlarm.getId().getId(), alarmInfo.getId().getId());
// Acknowledge alarm
client.ackAlarm(searchAlarm.getId().getId().toString());
// Verify alarm is acknowledged
Alarm ackedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString());
assertEquals(AlarmStatus.ACTIVE_ACK, ackedAlarm.getStatus());
// Clear alarm
client.clearAlarm(searchAlarm.getId().getId().toString());
// Verify alarm is cleared
Alarm clearedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString());
assertEquals(AlarmStatus.CLEARED_ACK, clearedAlarm.getStatus());
// Get highest severity alarm for device
AlarmSeverity highestSeverity = client.getHighestAlarmSeverity(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), null, null, null);
assertNotNull(highestSeverity);
assertEquals(AlarmSeverity.CRITICAL, highestSeverity);
// Assign alarm to customer
client.assignAlarm(createdAlarms.get(0).getId().getId().toString(), clientTenantAdmin.getId().getId().toString());
// Verify assignment
Alarm assignedAlarm = client.getAlarmById(createdAlarms.get(0).getId().getId().toString());
assertEquals(clientTenantAdmin.getId().getId(), assignedAlarm.getAssigneeId().getId());
// Unassign alarm
client.unassignAlarm(createdAlarms.get(0).getId().getId().toString());
// Verify unassignment
Alarm unassignedAlarm = client.getAlarmById(createdAlarms.get(0).getId().getId().toString());
assertNull(unassignedAlarm.getAssigneeId());
// Get alarm types
PageDataEntitySubtype pageDataEntitySubtype = client.getAlarmTypes(100, 0, null, null);
assertEquals(2, pageDataEntitySubtype.getData().size());
List<String> alarmTypes = pageDataEntitySubtype.getData().stream()
.map(EntitySubtype::getType)
.collect(Collectors.toList());
assertTrue(alarmTypes.containsAll(List.of("Temperature Alarm", "Connection Alarm")));
// Get alarms V2 (alternative endpoint)
PageDataAlarmInfo alarmsV2 = client.getAlarmsV2(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), 100, 0, null, null, null, null, null, null, null, null, null);
assertNotNull(alarmsV2);
assertEquals(1, alarmsV2.getData().size());
// Get all alarms V2
PageDataAlarmInfo allAlarmsV2 = client.getAllAlarmsV2(100, 0, null, null, null, null, null, null, null, null, null);
assertEquals(2, allAlarmsV2.getData().size());
// Delete alarm
UUID alarmToDeleteId = createdAlarms.get(0).getId().getId();
client.deleteAlarm(alarmToDeleteId.toString());
// Verify the alarm is deleted (should return 404)
assertReturns404(() ->
client.getAlarmById(alarmToDeleteId.toString())
);
// Verify count after deletion
PageDataAlarmInfo alarmsAfterDelete = client.getAllAlarms(100, 0, null, null, null, null, null, null, null, null, null);
assertEquals(initialSize - 1, alarmsAfterDelete.getData().size());
}
}

106
application/src/test/java/org/thingsboard/server/client/AlarmCommentApiClientTest.java

@ -0,0 +1,106 @@
/**
* 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.client;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Test;
import org.thingsboard.client.model.Alarm;
import org.thingsboard.client.model.AlarmComment;
import org.thingsboard.client.model.AlarmCommentInfo;
import org.thingsboard.client.model.AlarmSeverity;
import org.thingsboard.client.model.Device;
import org.thingsboard.client.model.PageDataAlarmCommentInfo;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@DaoSqlTest
public class AlarmCommentApiClientTest extends AbstractApiClientTest {
@Test
public void testAlarmComments() throws Exception {
long timestamp = System.currentTimeMillis();
// Create device for alarm
Device device = new Device();
device.setName("Device_For_Comments_" + timestamp);
device.setType("default");
Device createdDevice = client.saveDevice(device, null, null, null, null);
// Create alarm
Alarm alarm = new Alarm();
alarm.setType("Temperature Alarm");
alarm.setSeverity(AlarmSeverity.CRITICAL);
alarm.setOriginator(createdDevice.getId());
Alarm createdAlarm = client.saveAlarm(alarm);
String alarmId = createdAlarm.getId().getId().toString();
List<AlarmComment> createdComments = new ArrayList<>();
// Create multiple comments
for (int i = 0; i < 5; i++) {
AlarmComment alarmComment = new AlarmComment();
String message = "Test comment #" + i + " at " + timestamp;
ObjectNode comment = OBJECT_MAPPER.createObjectNode().put("message", message);
alarmComment.setComment(comment);
AlarmComment commentInfo = client.saveAlarmComment(alarmId, alarmComment);
assertNotNull(commentInfo);
assertNotNull(commentInfo.getId());
JsonNode commentValue = commentInfo.getComment();
assertEquals(message, commentValue.get("message").asText());
assertNotNull(commentInfo.getCreatedTime());
createdComments.add(commentInfo);
}
// Get all comments for the alarm
PageDataAlarmCommentInfo allComments = client.getAlarmComments(alarmId, 100, 0, null, null);
assertEquals("Expected 5 comments", 5, allComments.getData().size());
// Update a comment
AlarmComment commentToUpdate = createdComments.get(2);
JsonNode comment = commentToUpdate.getComment();
((ObjectNode) comment).put("message", "New comment");
commentToUpdate.setComment(comment);
AlarmComment updatedComment = client.saveAlarmComment(alarmId, commentToUpdate);
assertEquals("New comment", updatedComment.getComment().get("message").asText());
// Delete a comment
UUID commentToDeleteId = createdComments.get(0).getId().getId();
client.deleteAlarmComment(alarmId, commentToDeleteId.toString());
// Verify comment was updated to "deleted"
PageDataAlarmCommentInfo commentsAfterDelete = client.getAlarmComments(alarmId, 100, 0, null, null);
List<AlarmCommentInfo> data = commentsAfterDelete.getData();
AlarmCommentInfo deletedComment = data.stream()
.filter(alarmCommentInfo -> alarmCommentInfo.getId().getId().equals(commentToDeleteId))
.findFirst()
.get();
assertEquals("User " + clientTenantAdmin.getEmail() + " deleted his comment", deletedComment.getComment().get("text").asText());
}
}

117
application/src/test/java/org/thingsboard/server/client/ApiKeyApiClientTest.java

@ -0,0 +1,117 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.ApiKey;
import org.thingsboard.client.model.ApiKeyInfo;
import org.thingsboard.client.model.PageDataApiKeyInfo;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class ApiKeyApiClientTest extends AbstractApiClientTest {
@Test
public void testApiKeyLifecycle() throws Exception {
String userId = clientTenantAdmin.getId().getId().toString();
ApiKeyInfo request = new ApiKeyInfo();
request.setDescription("Test API key");
request.setUserId(clientTenantAdmin.getId());
request.setEnabled(true);
ApiKey created = client.saveApiKey(request);
assertNotNull(created);
assertNotNull(created.getId());
assertNotNull(created.getValue());
assertFalse(created.getValue().isBlank());
assertEquals("Test API key", created.getDescription());
UUID keyId = created.getId().getId();
PageDataApiKeyInfo keysPage = client.getUserApiKeys(userId, 100, 0, null, null, null);
assertNotNull(keysPage);
assertNotNull(keysPage.getData());
assertTrue("Newly created API key should appear in user's key list",
keysPage.getData().stream()
.anyMatch(k -> k.getId().getId().equals(keyId)));
client.deleteApiKey(keyId);
PageDataApiKeyInfo keysAfterDelete = client.getUserApiKeys(userId, 100, 0, null, null, null);
assertTrue("Deleted API key should not appear in user's key list",
keysAfterDelete.getData().stream()
.noneMatch(k -> k.getId().getId().equals(keyId)));
}
@Test
public void testEnableDisableApiKey() throws Exception {
ApiKeyInfo request = new ApiKeyInfo();
request.setDescription("Enable/disable test key");
request.setUserId(clientTenantAdmin.getId());
request.setEnabled(true);
ApiKey created = client.saveApiKey(request);
assertNotNull(created);
UUID keyId = created.getId().getId();
ApiKeyInfo disabled = client.enableApiKey(keyId, false);
assertNotNull(disabled);
assertEquals(Boolean.FALSE, disabled.getEnabled());
ApiKeyInfo enabled = client.enableApiKey(keyId, true);
assertNotNull(enabled);
assertEquals(Boolean.TRUE, enabled.getEnabled());
client.deleteApiKey(keyId);
}
@Test
public void testGetUserApiKeys() throws Exception {
String userId = clientTenantAdmin.getId().getId().toString();
int initialCount = client.getUserApiKeys(userId, 100, 0, null, null, null)
.getData().size();
UUID[] createdIds = new UUID[3];
for (int i = 0; i < 3; i++) {
ApiKeyInfo request = new ApiKeyInfo();
request.setDescription("Paging test key " + i);
request.setUserId(clientTenantAdmin.getId());
createdIds[i] = client.saveApiKey(request).getId().getId();
}
PageDataApiKeyInfo afterCreate = client.getUserApiKeys(userId, 100, 0, null, null, null);
assertEquals(initialCount + 3, afterCreate.getData().size());
assertEquals(Long.valueOf(initialCount + 3), afterCreate.getTotalElements());
PageDataApiKeyInfo page1 = client.getUserApiKeys(userId, 2, 0, null, null, null);
assertEquals(2, page1.getData().size());
assertTrue(page1.getHasNext());
for (UUID id : createdIds) {
client.deleteApiKey(id);
}
}
}

84
application/src/test/java/org/thingsboard/server/client/AssetApiClientTest.java

@ -0,0 +1,84 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.Asset;
import org.thingsboard.client.model.PageDataAsset;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@DaoSqlTest
public class AssetApiClientTest extends AbstractApiClientTest {
@Test
public void testAssetLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<Asset> createdAssets = new ArrayList<>();
// create 20 assets
for (int i = 0; i < 20; i++) {
Asset asset = new Asset();
String assetName = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i;
asset.setName(assetName);
asset.setLabel("Test Asset " + i);
asset.setType(((i % 2 == 0) ? "default" : "building"));
Asset createdAsset = client.saveAsset(asset, null, null, null);
assertNotNull(createdAsset);
assertNotNull(createdAsset.getId());
assertEquals(assetName, createdAsset.getName());
createdAssets.add(createdAsset);
}
// find all, check count
PageDataAsset allAssets = client.getTenantAssets(100, 0, null, null, null, null);
assertNotNull(allAssets);
assertNotNull(allAssets.getData());
int initialSize = allAssets.getData().size();
assertEquals("Expected at least 20 assets, but got " + allAssets.getData().size(), 20, initialSize);
//find all with search text, check count
PageDataAsset allAssetsBySearchText = client.getTenantAssets(100, 0, null, TEST_PREFIX_2, null, null);
assertEquals("Expected exactly 10 test assets", 10, allAssetsBySearchText.getData().size());
// find by id
Asset searchAsset = createdAssets.get(10);
Asset asset = client.getAssetById(searchAsset.getId().getId().toString());
assertEquals(searchAsset.getName(), asset.getName());
// delete asset
UUID assetToDeleteId = createdAssets.get(0).getId().getId();
client.deleteAsset(assetToDeleteId.toString());
// Verify the asset is deleted
PageDataAsset assetsAfterDelete = client.getTenantAssets(100, 0, null, null, null, null);
assertEquals(initialSize - 1, assetsAfterDelete.getData().size());
assertReturns404(() ->
client.getAssetById(assetToDeleteId.toString())
);
}
}

135
application/src/test/java/org/thingsboard/server/client/AssetProfileApiClientTest.java

@ -0,0 +1,135 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.AssetProfile;
import org.thingsboard.client.model.AssetProfileInfo;
import org.thingsboard.client.model.EntityInfo;
import org.thingsboard.client.model.PageDataAssetProfile;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class AssetProfileApiClientTest extends AbstractApiClientTest {
@Test
public void testAssetProfileLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<AssetProfile> createdProfiles = new ArrayList<>();
// Get initial count (there should be a default profile)
PageDataAssetProfile initialProfiles = client.getAssetProfiles(100, 0, null, null, null);
assertNotNull(initialProfiles);
int initialSize = initialProfiles.getData().size();
assertTrue("Expected at least 1 default asset profile", initialSize == 1);
// Get default asset profile info
AssetProfileInfo defaultProfileInfo = client.getDefaultAssetProfileInfo();
assertNotNull(defaultProfileInfo);
assertEquals(defaultProfileInfo.getName(), "default");
// Create multiple asset profiles
for (int i = 0; i < 5; i++) {
AssetProfile profile = new AssetProfile();
profile.setName("Test Asset Profile " + timestamp + "_" + i);
profile.setDescription("Test description " + i);
AssetProfile created = client.saveAssetProfile(profile);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals(profile.getName(), created.getName());
assertEquals(profile.getDescription(), created.getDescription());
assertFalse(created.getDefault());
createdProfiles.add(created);
}
// Find all, check count
PageDataAssetProfile allProfiles = client.getAssetProfiles(100, 0, null, null, null);
assertNotNull(allProfiles);
assertEquals(initialSize + 5, allProfiles.getData().size());
// Find all with text search
PageDataAssetProfile filteredProfiles = client.getAssetProfiles(100, 0, "Test Asset Profile " + timestamp, null, null);
assertEquals(5, filteredProfiles.getData().size());
// Get by id
AssetProfile searchProfile = createdProfiles.get(2);
AssetProfile fetchedProfile = client.getAssetProfileById(searchProfile.getId().getId().toString(), false);
assertEquals(searchProfile.getName(), fetchedProfile.getName());
assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription());
// Update asset profile
fetchedProfile.setDescription("Updated description");
AssetProfile updatedProfile = client.saveAssetProfile(fetchedProfile);
assertEquals("Updated description", updatedProfile.getDescription());
assertEquals(fetchedProfile.getName(), updatedProfile.getName());
// Get asset profile info by id
AssetProfileInfo profileInfo = client.getAssetProfileInfoById(searchProfile.getId().getId().toString());
assertNotNull(profileInfo);
assertEquals(searchProfile.getName(), profileInfo.getName());
// Get asset profile infos (paginated)
PageDataAssetProfile profileInfos = client.getAssetProfiles(100, 0, null, null, null);
assertNotNull(profileInfos);
assertEquals(initialSize + 5, profileInfos.getData().size());
// Set a profile as default
AssetProfile profileToSetDefault = createdProfiles.get(1);
AssetProfile newDefault = client.setDefaultAssetProfile(profileToSetDefault.getId().getId().toString());
assertNotNull(newDefault);
assertTrue(newDefault.getDefault());
// Verify default profile info now points to the new default
AssetProfileInfo newDefaultInfo = client.getDefaultAssetProfileInfo();
assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName());
// Get asset profile names
List<EntityInfo> profileNames = client.getAssetProfileNames(false);
assertNotNull(profileNames);
assertEquals(createdProfiles.size() + 1, profileNames.size());
// Delete asset profile (cannot delete the default one, so delete a non-default one)
UUID profileToDeleteId = createdProfiles.get(0).getId().getId();
client.deleteAssetProfile(profileToDeleteId.toString());
// Verify the profile is deleted
assertReturns404(() ->
client.getAssetProfileById(profileToDeleteId.toString(), false));
// Verify count after deletion
PageDataAssetProfile profilesAfterDelete = client.getAssetProfiles(100, 0, null, null, null);
assertEquals(initialSize + 4, profilesAfterDelete.getData().size());
// Restore original default profile
AssetProfile originalDefault = initialProfiles.getData().stream()
.filter(AssetProfile::getDefault)
.findFirst()
.orElseThrow();
client.setDefaultAssetProfile(originalDefault.getId().getId().toString());
}
}

286
application/src/test/java/org/thingsboard/server/client/CalculatedFieldApiClientTest.java

@ -0,0 +1,286 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.AlarmCalculatedFieldConfiguration;
import org.thingsboard.client.model.AlarmConditionValueAlarmSchedule;
import org.thingsboard.client.model.AlarmRule;
import org.thingsboard.client.model.AlarmSeverity;
import org.thingsboard.client.model.Argument;
import org.thingsboard.client.model.ArgumentType;
import org.thingsboard.client.model.CalculatedField;
import org.thingsboard.client.model.CalculatedFieldType;
import org.thingsboard.client.model.Device;
import org.thingsboard.client.model.EntityType;
import org.thingsboard.client.model.PageDataCalculatedField;
import org.thingsboard.client.model.ReferencedEntityKey;
import org.thingsboard.client.model.SimpleAlarmCondition;
import org.thingsboard.client.model.SimpleCalculatedFieldConfiguration;
import org.thingsboard.client.model.SpecificTimeSchedule;
import org.thingsboard.client.model.TbelAlarmConditionExpression;
import org.thingsboard.client.model.TimeSeriesOutput;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class CalculatedFieldApiClientTest extends AbstractApiClientTest {
@Test
public void testCalculatedFieldLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<CalculatedField> createdFields = new ArrayList<>();
// create devices to attach calculated fields to
Device device1 = new Device();
device1.setName("CalcFieldDevice1_" + timestamp);
device1.setType("default");
Device createdDevice1 = client.saveDevice(device1, null, null, null, null);
Device device2 = new Device();
device2.setName("CalcFieldDevice2_" + timestamp);
device2.setType("default");
Device createdDevice2 = client.saveDevice(device2, null, null, null, null);
// create calculated fields on device1
for (int i = 0; i < 5; i++) {
CalculatedField cf = new CalculatedField();
cf.setName(TEST_PREFIX + "CalcField_" + timestamp + "_" + i);
cf.setType(CalculatedFieldType.SIMPLE);
cf.setEntityId(createdDevice1.getId());
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument arg = new Argument();
ReferencedEntityKey refKey = new ReferencedEntityKey();
refKey.setKey("temperature");
refKey.setType(ArgumentType.TS_LATEST);
arg.setRefEntityKey(refKey);
config.putArgumentsItem("temp", arg);
config.setExpression("temp * " + (i + 1));
TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("scaledTemp_" + i);
config.setOutput(output);
cf.setConfiguration(config);
CalculatedField created = client.saveCalculatedField(cf);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals(cf.getName(), created.getName());
assertEquals(CalculatedFieldType.SIMPLE, created.getType());
createdFields.add(created);
}
// create calculated fields on device2
for (int i = 0; i < 3; i++) {
CalculatedField cf = new CalculatedField();
cf.setName(TEST_PREFIX + "CalcField2_" + timestamp + "_" + i);
cf.setType(CalculatedFieldType.SIMPLE);
cf.setEntityId(createdDevice2.getId());
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument arg = new Argument();
ReferencedEntityKey refKey = new ReferencedEntityKey();
refKey.setKey("humidity");
refKey.setType(ArgumentType.TS_LATEST);
arg.setRefEntityKey(refKey);
config.putArgumentsItem("hum", arg);
config.setExpression("hum + " + i);
TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("adjustedHumidity_" + i);
config.setOutput(output);
cf.setConfiguration(config);
CalculatedField created = client.saveCalculatedField(cf);
assertNotNull(created);
createdFields.add(created);
}
// get calculated fields by entity id for device1
PageDataCalculatedField device1Fields = client.getCalculatedFieldsByEntityId(
EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(),
100, 0, CalculatedFieldType.SIMPLE, null, null, null);
assertNotNull(device1Fields);
assertEquals(5, device1Fields.getData().size());
// get calculated fields by entity id for device2
PageDataCalculatedField device2Fields = client.getCalculatedFieldsByEntityId(
EntityType.DEVICE.toString(), createdDevice2.getId().getId().toString(),
100, 0, CalculatedFieldType.SIMPLE, null, null, null);
assertEquals(3, device2Fields.getData().size());
// get by id
CalculatedField searchField = createdFields.get(2);
CalculatedField fetchedField = client.getCalculatedFieldById(searchField.getId().getId().toString());
assertEquals(searchField.getName(), fetchedField.getName());
assertEquals(searchField.getType(), fetchedField.getType());
assertNotNull(fetchedField.getConfiguration());
SimpleCalculatedFieldConfiguration fetchedConfig =
(SimpleCalculatedFieldConfiguration) fetchedField.getConfiguration();
assertEquals("temp * 3", fetchedConfig.getExpression());
// update calculated field
fetchedField.setName(fetchedField.getName() + "_updated");
fetchedConfig.setExpression("temp * 100");
CalculatedField updatedField = client.saveCalculatedField(fetchedField);
assertEquals(fetchedField.getName(), updatedField.getName());
SimpleCalculatedFieldConfiguration updatedConfig =
(SimpleCalculatedFieldConfiguration) updatedField.getConfiguration();
assertEquals("temp * 100", updatedConfig.getExpression());
// delete calculated field
UUID fieldToDeleteId = createdFields.get(0).getId().getId();
client.deleteCalculatedField(fieldToDeleteId.toString());
// verify deletion
assertReturns404(() ->
client.getCalculatedFieldById(fieldToDeleteId.toString())
);
PageDataCalculatedField device1FieldsAfterDelete = client.getCalculatedFieldsByEntityId(
EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(),
100, 0, null, null, null, null);
assertEquals(4, device1FieldsAfterDelete.getData().size());
}
@Test
public void testAlarmCalculatedFieldLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
// create a device to attach the alarm calculated field to
Device device = new Device();
device.setName("AlarmCalcFieldDevice_" + timestamp);
device.setType("default");
Device createdDevice = client.saveDevice(device, null, null, null, null);
// build the alarm calculated field configuration
AlarmCalculatedFieldConfiguration config = new AlarmCalculatedFieldConfiguration();
// argument: temperature time-series
Argument tempArg = new Argument();
ReferencedEntityKey refKey = new ReferencedEntityKey();
refKey.setKey("temperature");
refKey.setType(ArgumentType.TS_LATEST);
tempArg.setRefEntityKey(refKey);
config.putArgumentsItem("temp", tempArg);
// create rule: HIGH_TEMPERATURE when temp > 50 (TBEL expression)
TbelAlarmConditionExpression createExpression = new TbelAlarmConditionExpression();
createExpression.setExpression("return temp > 50;");
SimpleAlarmCondition createCondition = new SimpleAlarmCondition();
createCondition.setExpression(createExpression);
SpecificTimeSchedule specificTimeSchedule = new SpecificTimeSchedule().addDaysOfWeekItem(3);
AlarmConditionValueAlarmSchedule schedule = new AlarmConditionValueAlarmSchedule().staticValue(specificTimeSchedule);
createCondition.setSchedule(schedule);
AlarmRule createRule = new AlarmRule();
createRule.setCondition(createCondition);
createRule.setAlarmDetails("Temperature is too high: ${temp}");
config.setCreateRules(Map.of(
AlarmSeverity.CRITICAL.name(), createRule
));
// clear rule: when temp drops below 30
TbelAlarmConditionExpression clearExpression = new TbelAlarmConditionExpression();
clearExpression.setExpression("return temp < 30;");
SimpleAlarmCondition clearCondition = new SimpleAlarmCondition();
clearCondition.setExpression(clearExpression);
AlarmRule clearRule = new AlarmRule();
clearRule.setCondition(clearCondition);
config.setClearRule(clearRule);
config.setPropagate(true);
config.setPropagateToOwner(false);
// create calculated field
CalculatedField cf = new CalculatedField();
cf.setName(TEST_PREFIX + "AlarmCalcField_" + timestamp);
cf.setType(CalculatedFieldType.ALARM);
cf.setEntityId(createdDevice.getId());
cf.setConfiguration(config);
CalculatedField created = client.saveCalculatedField(cf);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals(cf.getName(), created.getName());
assertEquals(CalculatedFieldType.ALARM, created.getType());
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) created.getConfiguration();
AlarmConditionValueAlarmSchedule createdSchedule = configuration.getCreateRules().get(AlarmSeverity.CRITICAL.name()).getCondition().getSchedule();
SpecificTimeSchedule staticSchedule = (SpecificTimeSchedule) createdSchedule.getStaticValue();
assertEquals(Set.of(3), staticSchedule.getDaysOfWeek());
// get by id and verify configuration
CalculatedField fetched = client.getCalculatedFieldById(created.getId().getId().toString());
assertNotNull(fetched);
assertEquals(created.getName(), fetched.getName());
assertEquals(CalculatedFieldType.ALARM, fetched.getType());
assertNotNull(fetched.getConfiguration());
AlarmCalculatedFieldConfiguration fetchedConfig =
(AlarmCalculatedFieldConfiguration) fetched.getConfiguration();
assertNotNull(fetchedConfig.getCreateRules());
assertEquals(1, fetchedConfig.getCreateRules().size());
assertTrue(fetchedConfig.getCreateRules().containsKey("CRITICAL"));
assertNotNull(fetchedConfig.getClearRule());
assertEquals(Boolean.TRUE, fetchedConfig.getPropagate());
// update: add a second create rule for CRITICAL_TEMPERATURE
TbelAlarmConditionExpression criticalExpression = new TbelAlarmConditionExpression();
criticalExpression.setExpression("return temp > 80;");
SimpleAlarmCondition criticalCondition = new SimpleAlarmCondition();
criticalCondition.setExpression(criticalExpression);
AlarmRule criticalRule = new AlarmRule();
criticalRule.setCondition(criticalCondition);
fetchedConfig.putCreateRulesItem(AlarmSeverity.INDETERMINATE.name(), criticalRule);
fetched.setConfiguration(fetchedConfig);
CalculatedField updated = client.saveCalculatedField(fetched);
AlarmCalculatedFieldConfiguration updatedConfig =
(AlarmCalculatedFieldConfiguration) updated.getConfiguration();
assertEquals(2, updatedConfig.getCreateRules().size());
assertTrue(updatedConfig.getCreateRules().containsKey("INDETERMINATE"));
// filter by entity and ALARM type
PageDataCalculatedField deviceFields = client.getCalculatedFieldsByEntityId(
EntityType.DEVICE.toString(), createdDevice.getId().getId().toString(),
100, 0, CalculatedFieldType.ALARM, null, null, null);
assertNotNull(deviceFields);
assertEquals(1, deviceFields.getData().size());
// delete and verify
UUID fieldId = created.getId().getId();
client.deleteCalculatedField(fieldId.toString());
assertReturns404(() -> client.getCalculatedFieldById(fieldId.toString()));
}
}

113
application/src/test/java/org/thingsboard/server/client/CustomerApiClientTest.java

@ -0,0 +1,113 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.Customer;
import org.thingsboard.client.model.Device;
import org.thingsboard.client.model.PageDataCustomer;
import org.thingsboard.client.model.PageDataDevice;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@DaoSqlTest
public class CustomerApiClientTest extends AbstractApiClientTest {
@Test
public void testCustomerLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<Customer> createdCustomers = new ArrayList<>();
// create 20 customers
for (int i = 0; i < 20; i++) {
Customer customer = new Customer();
String customerTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i;
customer.setTitle(customerTitle);
customer.setEmail("customer_" + timestamp + "_" + i + "@test.com");
Customer createdCustomer = client.saveCustomer(customer, null, null, null);
assertNotNull(createdCustomer);
assertNotNull(createdCustomer.getId());
assertEquals(customerTitle, createdCustomer.getTitle());
createdCustomers.add(createdCustomer);
}
// find all, check count (includes savedClientCustomer from AbstractApiClientTest setup)
PageDataCustomer allCustomers = client.getCustomers(100, 0, null, null, null);
assertNotNull(allCustomers);
assertNotNull(allCustomers.getData());
int initialSize = allCustomers.getData().size();
assertEquals("Expected 21 customers (20 created + 1 from setup), but got " + initialSize, 21, initialSize);
// find all with search text, check count
PageDataCustomer filteredCustomers = client.getCustomers(100, 0, TEST_PREFIX_2, null, null);
assertEquals("Expected exactly 10 customers matching prefix", 10, filteredCustomers.getData().size());
// find by id
Customer searchCustomer = createdCustomers.get(10);
Customer fetchedCustomer = client.getCustomerById(searchCustomer.getId().getId().toString());
assertEquals(searchCustomer.getTitle(), fetchedCustomer.getTitle());
// find by title
Customer fetchedByTitle = client.getTenantCustomer(searchCustomer.getTitle());
assertEquals(searchCustomer.getId().getId(), fetchedByTitle.getId().getId());
// update customer
fetchedCustomer.setCity("New York");
fetchedCustomer.setCountry("US");
Customer updatedCustomer = client.saveCustomer(fetchedCustomer, null, null, null);
assertEquals("New York", updatedCustomer.getCity());
assertEquals("US", updatedCustomer.getCountry());
// assign device to customer and verify
Device device = new Device();
device.setName("CustomerTestDevice_" + timestamp);
device.setType("default");
Device createdDevice = client.saveDevice(device, null, null, null, null);
String customerId = createdCustomers.get(0).getId().getId().toString();
client.assignDeviceToCustomer(customerId, createdDevice.getId().getId().toString());
PageDataDevice customerDevices = client.getCustomerDevices(customerId, 100, 0, null, null, null, null);
assertEquals(1, customerDevices.getData().size());
assertEquals(createdDevice.getName(), customerDevices.getData().get(0).getName());
// unassign device from customer
client.unassignDeviceFromCustomer(createdDevice.getId().getId().toString());
PageDataDevice devicesAfterUnassign = client.getCustomerDevices(customerId, 100, 0, null, null, null, null);
assertEquals(0, devicesAfterUnassign.getData().size());
// delete customer
UUID customerToDeleteId = createdCustomers.get(0).getId().getId();
client.deleteCustomer(customerToDeleteId.toString());
// verify deletion
PageDataCustomer customersAfterDelete = client.getCustomers(100, 0, null, null, null);
assertEquals(initialSize - 1, customersAfterDelete.getData().size());
assertReturns404(() ->
client.getCustomerById(customerToDeleteId.toString())
);
}
}

123
application/src/test/java/org/thingsboard/server/client/DashboardApiClientTest.java

@ -0,0 +1,123 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.Dashboard;
import org.thingsboard.client.model.DashboardInfo;
import org.thingsboard.client.model.PageDataDashboardInfo;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class DashboardApiClientTest extends AbstractApiClientTest {
@Test
public void testDashboardLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
// create 20 dashboards
for (int i = 0; i < 20; i++) {
Dashboard dashboard = new Dashboard();
String dashboardTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i;
dashboard.setTitle(dashboardTitle);
client.saveDashboard(dashboard, null);
}
// find all, check count
PageDataDashboardInfo allDashboards = client.getTenantDashboards(100, 0, null, null, null, null);
assertNotNull(allDashboards);
assertNotNull(allDashboards.getData());
int initialSize = allDashboards.getData().size();
assertEquals("Expected 20 dashboards, but got " + initialSize, 20, initialSize);
List<DashboardInfo> createdDashboards = allDashboards.getData();
// find all with search text, check count
PageDataDashboardInfo filteredDashboards = client.getTenantDashboards(100, 0, null, TEST_PREFIX_2, null, null);
assertEquals("Expected exactly 10 dashboards matching prefix", 10, filteredDashboards.getData().size());
// find by id
DashboardInfo searchDashboard = createdDashboards.get(10);
DashboardInfo fetchedDashboard = client.getDashboardInfoById(searchDashboard.getId().getId().toString());
assertEquals(searchDashboard.getTitle(), fetchedDashboard.getTitle());
// update dashboard
Dashboard dashboardToUpdate = new Dashboard();
dashboardToUpdate.setId(fetchedDashboard.getId());
dashboardToUpdate.setTitle(fetchedDashboard.getTitle() + "_updated");
dashboardToUpdate.setVersion(fetchedDashboard.getVersion());
client.saveDashboard(dashboardToUpdate, null);
DashboardInfo updatedDashboard = client.getDashboardInfoById(fetchedDashboard.getId().getId().toString());
assertEquals(fetchedDashboard.getTitle() + "_updated", updatedDashboard.getTitle());
// assign dashboard to customer and verify
String customerId = savedClientCustomer.getId().getId().toString();
String dashboardId = createdDashboards.get(0).getId().getId().toString();
client.assignDashboardToCustomer(customerId, dashboardId);
PageDataDashboardInfo customerDashboards = client.getCustomerDashboards(customerId, 100, 0, null, null, null, null);
assertEquals(1, customerDashboards.getData().size());
assertEquals(createdDashboards.get(0).getTitle(), customerDashboards.getData().get(0).getTitle());
// unassign dashboard from customer
client.unassignDashboardFromCustomer(customerId, dashboardId);
PageDataDashboardInfo dashboardsAfterUnassign = client.getCustomerDashboards(customerId, 100, 0, null, null, null, null);
assertEquals(0, dashboardsAfterUnassign.getData().size());
// make dashboard public and verify
client.assignDashboardToPublicCustomer(dashboardId);
DashboardInfo publicDashboard = client.getDashboardInfoById(dashboardId);
assertNotNull(publicDashboard.getAssignedCustomers());
assertTrue(publicDashboard.getAssignedCustomers().size() > 0);
// remove public access
client.unassignDashboardFromPublicCustomer(dashboardId);
// delete dashboard
UUID dashboardToDeleteId = createdDashboards.get(0).getId().getId();
client.deleteDashboard(dashboardToDeleteId.toString());
// verify deletion
PageDataDashboardInfo dashboardsAfterDelete = client.getTenantDashboards(100, 0, null, null, null, null);
assertEquals(initialSize - 1, dashboardsAfterDelete.getData().size());
assertReturns404(() ->
client.getDashboardInfoById(dashboardToDeleteId.toString())
);
}
@Test
public void testGetServerTime() throws Exception {
Long serverTime = client.getServerTime();
assertNotNull(serverTime);
}
@Test
public void testGetMaxDatapointsLimit() throws Exception {
Long maxDatapointsLimit = client.getMaxDatapointsLimit();
assertNotNull(maxDatapointsLimit);
}
}

114
application/src/test/java/org/thingsboard/server/client/DeviceApiClientTest.java

@ -0,0 +1,114 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.Device;
import org.thingsboard.client.model.DeviceCredentials;
import org.thingsboard.client.model.DeviceCredentialsType;
import org.thingsboard.client.model.PageDataDevice;
import org.thingsboard.client.model.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@DaoSqlTest
public class DeviceApiClientTest extends AbstractApiClientTest {
@Test
public void testDeviceLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<Device> createdDevices = new ArrayList<>();
// create 20 devices
for (int i = 0; i < 20; i++) {
Device device = new Device();
String deviceName = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i;
device.setName(deviceName);
device.setLabel("Test Device " + i);
device.setType(((i % 2 == 0) ? "default" : "thermostat"));
Device createdDevice = client.saveDevice(device, null, null, null, null);
assertNotNull(createdDevice);
assertNotNull(createdDevice.getId());
assertEquals(deviceName, createdDevice.getName());
createdDevices.add(createdDevice);
}
// find all, check count
PageDataDevice allDevices = client.getTenantDevices(100, 0, null, null, null, null);
assertNotNull(allDevices);
assertNotNull(allDevices.getData());
int initialSize = allDevices.getData().size();
assertEquals("Expected at least 20 devices, but got " + allDevices.getData().size(), 20, initialSize);
//find all with search text, check count
PageDataDevice allDevicesBySearchText = client.getTenantDevices(10, 0, null, TEST_PREFIX_2, null, null);
assertEquals("Expected exactly 10 test devices", 10, allDevicesBySearchText.getData().size());
// find by id
Device searchDevice = createdDevices.get(10);
Device device = client.getDeviceById(searchDevice.getId().getId().toString());
assertEquals(searchDevice.getName(), device.getName());
// create device with credentials
Device deviceWithCreds = new Device();
deviceWithCreds.setName("device-with-creds");
DeviceCredentials creds = new DeviceCredentials();
creds.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
creds.setCredentialsId("TEST_ACCESS_TOKEN");
SaveDeviceWithCredentialsRequest request = new SaveDeviceWithCredentialsRequest();
request.setDevice(deviceWithCreds);
request.setCredentials(creds);
Device savedDeviceWithCreds = client.saveDeviceWithCredentials(request, null, null, null);
assertEquals("device-with-creds", savedDeviceWithCreds.getName());
// find credentials by device id
DeviceCredentials fetchedCreds = client.getDeviceCredentialsByDeviceId(savedDeviceWithCreds.getId().getId().toString());
assertEquals(creds.getCredentialsId(), fetchedCreds.getCredentialsId());
// delete device
UUID deviceToDeleteId = createdDevices.get(0).getId().getId();
client.deleteDevice(deviceToDeleteId.toString());
// Verify the device is deleted
PageDataDevice devicesAfterDelete = client.getTenantDevices(100, 0, null, null, null, null);
assertEquals(initialSize, devicesAfterDelete.getData().size());
assertReturns404(() ->
client.getDeviceById(deviceToDeleteId.toString()));
// assign device to customer
client.assignDeviceToCustomer(savedClientCustomer.getId().getId().toString(), savedDeviceWithCreds.getId().getId().toString());
// check customer devices
PageDataDevice pageDataDevice = client.getCustomerDevices(savedClientCustomer.getId().getId().toString(), 100, 0, null, null, null, null);
List<Device> data = pageDataDevice.getData();
assertEquals(1, data.size());
assertEquals(savedDeviceWithCreds.getName(), data.get(0).getName());
}
}

53
application/src/test/java/org/thingsboard/server/client/DeviceConnectivityApiClientTest.java

@ -0,0 +1,53 @@
/**
* 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.client;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.Test;
import org.thingsboard.client.model.Device;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
@DaoSqlTest
public class DeviceConnectivityApiClientTest extends AbstractApiClientTest {
@Test
public void testGetDevicePublishTelemetryCommands() throws Exception {
Device device = new Device();
device.setName(TEST_PREFIX + System.currentTimeMillis());
device.setType("default");
Device savedDevice = client.saveDevice(device, null, null, null, null);
String token = client.getDeviceCredentialsByDeviceId(savedDevice.getId().getId().toString()).getCredentialsId();
String deviceId = savedDevice.getId().getId().toString();
JsonNode commands = client.getDevicePublishTelemetryCommands(deviceId);
assertEquals("curl -v -X POST http://localhost:8080/api/v1/" + token + "/telemetry --header Content-Type:application/json --data \"{temperature:25}\"", commands.get("http").get("http").asText());
assertEquals("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry -u \"" + token + "\" -m \"{temperature:25}\"", commands.get("mqtt").get("mqtt").asText());
assertEquals("coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://localhost:5683/api/v1/" + token + "/telemetry", commands.get("coap").get("coap").asText());
}
@Test
public void testGetDevicePublishTelemetryCommands_nonExistentDevice() {
String nonExistentId = UUID.randomUUID().toString();
assertReturns404(() -> client.getDevicePublishTelemetryCommands(nonExistentId));
}
}

157
application/src/test/java/org/thingsboard/server/client/DeviceProfileApiClientTest.java

@ -0,0 +1,157 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.DefaultDeviceProfileConfiguration;
import org.thingsboard.client.model.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.client.model.DeviceProfile;
import org.thingsboard.client.model.DeviceProfileData;
import org.thingsboard.client.model.DeviceProfileInfo;
import org.thingsboard.client.model.DeviceProfileType;
import org.thingsboard.client.model.DeviceTransportType;
import org.thingsboard.client.model.EntityInfo;
import org.thingsboard.client.model.PageDataDeviceProfile;
import org.thingsboard.client.model.PageDataDeviceProfileInfo;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class DeviceProfileApiClientTest extends AbstractApiClientTest {
@Test
public void testDeviceProfileLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<DeviceProfile> createdProfiles = new ArrayList<>();
// Get initial count (there should be a default profile)
PageDataDeviceProfile initialProfiles = client.getDeviceProfiles(100, 0, null, null, null);
assertNotNull(initialProfiles);
int initialSize = initialProfiles.getData().size();
assertTrue("Expected at least 1 default device profile", initialSize >= 1);
// Get default device profile info
DeviceProfileInfo defaultProfileInfo = client.getDefaultDeviceProfileInfo();
assertNotNull(defaultProfileInfo);
assertNotNull(defaultProfileInfo.getName());
// Create multiple device profiles
for (int i = 0; i < 5; i++) {
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setName("Test Device Profile " + timestamp + "_" + i);
deviceProfile.setDescription("Test description " + i);
deviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setTransportType(DeviceTransportType.DEFAULT);
DeviceProfileData deviceProfileData = new DeviceProfileData();
DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
configuration.setType(DeviceProfileType.DEFAULT.getValue());
deviceProfileData.setConfiguration(configuration);
DefaultDeviceProfileTransportConfiguration transportConf = new DefaultDeviceProfileTransportConfiguration();
transportConf.setType(DeviceTransportType.DEFAULT.getValue());
deviceProfileData.setTransportConfiguration(transportConf);
deviceProfile.setProfileData(deviceProfileData);
deviceProfile.setDefault(false);
deviceProfile.setDefaultRuleChainId(null);
DeviceProfile created = client.saveDeviceProfile(deviceProfile);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals(deviceProfile.getName(), created.getName());
assertEquals(deviceProfile.getDescription(), created.getDescription());
assertEquals(DeviceProfileType.DEFAULT, created.getType());
assertEquals(DeviceTransportType.DEFAULT, created.getTransportType());
assertFalse(created.getDefault());
createdProfiles.add(created);
}
// Find all, check count
PageDataDeviceProfile allProfiles = client.getDeviceProfiles(100, 0, null, null, null);
assertNotNull(allProfiles);
assertEquals(initialSize + 5, allProfiles.getData().size());
// Find all with text search
PageDataDeviceProfile filteredProfiles = client.getDeviceProfiles(100, 0, "Test Device Profile " + timestamp, null, null);
assertEquals(5, filteredProfiles.getData().size());
// Get by id
DeviceProfile searchProfile = createdProfiles.get(2);
DeviceProfile fetchedProfile = client.getDeviceProfileById(searchProfile.getId().getId().toString(), false);
assertEquals(searchProfile.getName(), fetchedProfile.getName());
assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription());
// Update device profile
fetchedProfile.setDescription("Updated description");
DeviceProfile updatedProfile = client.saveDeviceProfile(fetchedProfile);
assertEquals("Updated description", updatedProfile.getDescription());
assertEquals(fetchedProfile.getName(), updatedProfile.getName());
// Get device profile info by id
DeviceProfileInfo profileInfo = client.getDefaultDeviceProfileInfo();
assertNotNull(profileInfo);
assertEquals(searchProfile.getType().getValue().toLowerCase(), profileInfo.getName());
assertEquals(DeviceTransportType.DEFAULT, profileInfo.getTransportType());
// Get device profile infos (paginated)
PageDataDeviceProfileInfo profileInfos = client.getDeviceProfileInfos(100, 0, null, null, null, null);
assertNotNull(profileInfos);
assertEquals(initialSize + 5, profileInfos.getData().size());
// Set a profile as default
DeviceProfile profileToSetDefault = createdProfiles.get(1);
DeviceProfile newDefault = client.setDefaultDeviceProfile(profileToSetDefault.getId().getId().toString());
assertNotNull(newDefault);
assertTrue(newDefault.getDefault());
// Verify default profile info now points to the new default
DeviceProfileInfo newDefaultInfo = client.getDefaultDeviceProfileInfo();
assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName());
// Get device profile names
List<EntityInfo> profileNames = client.getDeviceProfileNames(false);
assertNotNull(profileNames);
assertEquals(createdProfiles.size() + 1, profileNames.size());
// Delete device profile (cannot delete the default one, so delete a non-default one)
UUID profileToDeleteId = createdProfiles.get(0).getId().getId();
client.deleteDeviceProfile(profileToDeleteId.toString());
// Verify the profile is deleted
assertReturns404(() ->
client.getDeviceProfileById(profileToDeleteId.toString(), false));
// Verify count after deletion
PageDataDeviceProfile profilesAfterDelete = client.getDeviceProfiles(100, 0, null, null, null);
assertEquals(initialSize + 4, profilesAfterDelete.getData().size());
// Restore original default profile
DeviceProfile originalDefault = initialProfiles.getData().stream()
.filter(DeviceProfile::getDefault)
.findFirst()
.orElseThrow();
client.setDefaultDeviceProfile(originalDefault.getId().getId().toString());
}
}

105
application/src/test/java/org/thingsboard/server/client/DomainApiClientTest.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.client;
import org.junit.After;
import org.junit.Test;
import org.thingsboard.client.ApiException;
import org.thingsboard.client.model.Domain;
import org.thingsboard.client.model.DomainInfo;
import org.thingsboard.client.model.PageDataDomainInfo;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@DaoSqlTest
public class DomainApiClientTest extends AbstractApiClientTest {
List<Domain> createdDomains = new ArrayList<>();
@After
public void afterDomainTest() {
createdDomains.forEach(domain -> {
try {
client.deleteDomain(domain.getId().getId());
} catch (ApiException e) {
// ignore
}
});
}
@Test
public void testDomainLifecycle() throws Exception {
client.login("sysadmin@thingsboard.org", "sysadmin");
long timestamp = System.currentTimeMillis();
// create 5 domains
for (int i = 0; i < 5; i++) {
Domain domain = new Domain();
domain.setName("domain." + i + ".com");
domain.setOauth2Enabled(false);
domain.setPropagateToEdge(false);
Domain created = client.saveDomain(domain, null);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals(domain.getName(), created.getName());
assertEquals(false, created.getOauth2Enabled());
createdDomains.add(created);
}
// list tenant domains with text search
PageDataDomainInfo filteredDomains = client.getTenantDomainInfos(100, 0,
"domain.", null, null);
assertNotNull(filteredDomains);
assertEquals(5, filteredDomains.getData().size());
// get domain info by id
Domain searchDomain = createdDomains.get(2);
DomainInfo fetchedInfo = client.getDomainInfoById(searchDomain.getId().getId());
assertEquals(searchDomain.getName(), fetchedInfo.getName());
assertEquals(searchDomain.getOauth2Enabled(), fetchedInfo.getOauth2Enabled());
assertNotNull(fetchedInfo.getOauth2ClientInfos());
// update domain
Domain domainToUpdate = createdDomains.get(3);
domainToUpdate.setPropagateToEdge(true);
Domain updatedDomain = client.saveDomain(domainToUpdate, null);
assertEquals(true, updatedDomain.getPropagateToEdge());
// delete domain
UUID domainToDeleteId = createdDomains.get(0).getId().getId();
createdDomains.remove(0);
client.deleteDomain(domainToDeleteId);
// verify deletion
assertReturns404(() ->
client.getDomainInfoById(domainToDeleteId)
);
PageDataDomainInfo domainsAfterDelete = client.getTenantDomainInfos(100, 0,
"domain.", null, null);
assertEquals(4, domainsAfterDelete.getData().size());
}
}

141
application/src/test/java/org/thingsboard/server/client/EdgeApiClientTest.java

@ -0,0 +1,141 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.Edge;
import org.thingsboard.client.model.EdgeInfo;
import org.thingsboard.client.model.PageDataEdge;
import org.thingsboard.client.model.PageDataEdgeInfo;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@DaoSqlTest
public class EdgeApiClientTest extends AbstractApiClientTest {
@Test
public void testEdgeLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<Edge> createdEdges = new ArrayList<>();
// create 5 edges
for (int i = 0; i < 5; i++) {
Edge edge = new Edge();
edge.setName(TEST_PREFIX + "Edge_" + timestamp + "_" + i);
edge.setType("gateway");
edge.setLabel("Test Edge " + i);
edge.setRoutingKey("routing_key_" + timestamp + "_" + i);
edge.setSecret("secret_key_" + timestamp + "_" + i);
Edge created = client.saveEdge(edge);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals(edge.getName(), created.getName());
assertEquals("gateway", created.getType());
assertNotNull(created.getRoutingKey());
assertNotNull(created.getSecret());
createdEdges.add(created);
}
// list tenant edges with text search
PageDataEdge filteredEdges = client.getTenantEdges(100, 0, null,
TEST_PREFIX + "Edge_" + timestamp, null, null);
assertNotNull(filteredEdges);
assertEquals(5, filteredEdges.getData().size());
// list tenant edges with type filter
PageDataEdge typedEdges = client.getTenantEdges(100, 0, "gateway",
TEST_PREFIX + "Edge_" + timestamp, null, null);
assertEquals(5, typedEdges.getData().size());
// get tenant edge infos
PageDataEdgeInfo edgeInfos = client.getTenantEdgeInfos(100, 0, null,
TEST_PREFIX + "Edge_" + timestamp, null, null);
assertEquals(5, edgeInfos.getData().size());
// get edge by id
Edge searchEdge = createdEdges.get(2);
Edge fetchedEdge = client.getEdgeById(searchEdge.getId().getId().toString());
assertEquals(searchEdge.getName(), fetchedEdge.getName());
assertEquals(searchEdge.getType(), fetchedEdge.getType());
assertEquals(searchEdge.getRoutingKey(), fetchedEdge.getRoutingKey());
// get edge by name
Edge fetchedByName = client.getTenantEdgeByName(searchEdge.getName());
assertEquals(searchEdge.getId().getId(), fetchedByName.getId().getId());
// get edges by list of ids
List<String> idsToFetch = List.of(
createdEdges.get(0).getId().getId().toString(),
createdEdges.get(1).getId().getId().toString()
);
List<Edge> edgeList = client.getEdgeList(idsToFetch);
assertEquals(2, edgeList.size());
// update edge
Edge edgeToUpdate = createdEdges.get(3);
edgeToUpdate.setLabel("Updated Label");
Edge updatedEdge = client.saveEdge(edgeToUpdate);
assertEquals("Updated Label", updatedEdge.getLabel());
// assign edge to customer
String customerId = savedClientCustomer.getId().getId().toString();
String edgeId = createdEdges.get(1).getId().getId().toString();
Edge assignedEdge = client.assignEdgeToCustomer(customerId, edgeId);
assertNotNull(assignedEdge.getCustomerId());
// get customer edges
PageDataEdge customerEdges = client.getCustomerEdges(customerId, 100, 0,
null, TEST_PREFIX + "Edge_" + timestamp, null, null);
assertEquals(1, customerEdges.getData().size());
// get customer edge infos
PageDataEdgeInfo customerEdgeInfos = client.getCustomerEdgeInfos(customerId, 100, 0,
null, TEST_PREFIX + "Edge_" + timestamp, null, null);
assertEquals(1, customerEdgeInfos.getData().size());
EdgeInfo edgeInfo = customerEdgeInfos.getData().get(0);
assertNotNull(edgeInfo.getCustomerTitle());
// unassign edge from customer
Edge unassignedEdge = client.unassignEdgeFromCustomer(edgeId);
assertNotNull(unassignedEdge);
PageDataEdge customerEdgesAfter = client.getCustomerEdges(customerId, 100, 0,
null, TEST_PREFIX + "Edge_" + timestamp, null, null);
assertEquals(0, customerEdgesAfter.getData().size());
// delete edge
UUID edgeToDeleteId = createdEdges.get(0).getId().getId();
client.deleteEdge(edgeToDeleteId.toString());
// verify deletion
assertReturns404(() ->
client.getEdgeById(edgeToDeleteId.toString())
);
PageDataEdge edgesAfterDelete = client.getTenantEdges(100, 0, null,
TEST_PREFIX + "Edge_" + timestamp, null, null);
assertEquals(4, edgesAfterDelete.getData().size());
}
}

289
application/src/test/java/org/thingsboard/server/client/EntityQueryApiClientTest.java

@ -0,0 +1,289 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.AliasEntityId;
import org.thingsboard.client.model.Asset;
import org.thingsboard.client.model.AssetTypeFilter;
import org.thingsboard.client.model.Device;
import org.thingsboard.client.model.DeviceTypeFilter;
import org.thingsboard.client.model.Direction;
import org.thingsboard.client.model.EntityData;
import org.thingsboard.client.model.EntityDataPageLink;
import org.thingsboard.client.model.EntityDataQuery;
import org.thingsboard.client.model.EntityDataSortOrder;
import org.thingsboard.client.model.EntityKey;
import org.thingsboard.client.model.EntityKeyType;
import org.thingsboard.client.model.EntityKeyValueType;
import org.thingsboard.client.model.EntityListFilter;
import org.thingsboard.client.model.EntityNameFilter;
import org.thingsboard.client.model.EntityType;
import org.thingsboard.client.model.FilterPredicateValueString;
import org.thingsboard.client.model.KeyFilter;
import org.thingsboard.client.model.PageDataEntityData;
import org.thingsboard.client.model.SingleEntityFilter;
import org.thingsboard.client.model.StringFilterPredicate;
import org.thingsboard.client.model.StringOperation;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class EntityQueryApiClientTest extends AbstractApiClientTest {
private static final String QUERY_TEST_PREFIX = "QueryTest_";
private EntityDataPageLink pageLink(int pageSize) {
return new EntityDataPageLink()
.pageSize(pageSize)
.page(0)
.sortOrder(new EntityDataSortOrder()
.key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"))
.direction(Direction.ASC));
}
@Test
public void testFindByDeviceTypeFilter() throws Exception {
long ts = System.currentTimeMillis();
String type1 = "temperatureSensor";
String type2 = "humiditySensor";
for (int i = 0; i < 3; i++) {
Device d = new Device();
d.setName(QUERY_TEST_PREFIX + "temp_" + ts + "_" + i);
d.setType(type1);
client.saveDevice(d, null, null, null, null);
}
for (int i = 0; i < 2; i++) {
Device d = new Device();
d.setName(QUERY_TEST_PREFIX + "hum_" + ts + "_" + i);
d.setType(type2);
client.saveDevice(d, null, null, null, null);
}
// filter by single device type
EntityDataQuery singleTypeQuery = new EntityDataQuery()
.entityFilter(new DeviceTypeFilter()
.deviceTypes(List.of(type1)))
.pageLink(pageLink(10))
.addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
PageDataEntityData result = client.findEntityDataByQuery(singleTypeQuery);
assertNotNull(result);
assertEquals(3, result.getTotalElements().intValue());
for (EntityData entity : result.getData()) {
assertNotNull(entity.getEntityId());
}
// filter by multiple device types
EntityDataQuery multiTypeQuery = new EntityDataQuery()
.entityFilter(new DeviceTypeFilter()
.deviceTypes(List.of(type1, type2)))
.pageLink(pageLink(10))
.addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
PageDataEntityData multiResult = client.findEntityDataByQuery(multiTypeQuery);
assertNotNull(multiResult);
assertEquals(5, multiResult.getTotalElements().intValue());
// filter by device type + name filter
EntityDataQuery nameFilterQuery = new EntityDataQuery()
.entityFilter(new DeviceTypeFilter()
.deviceTypes(List.of(type1, type2))
.deviceNameFilter(QUERY_TEST_PREFIX + "temp_" + ts))
.pageLink(pageLink(10))
.addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
PageDataEntityData nameResult = client.findEntityDataByQuery(nameFilterQuery);
assertNotNull(nameResult);
assertEquals(3, nameResult.getTotalElements().intValue());
}
@Test
public void testFindByEntityNameFilter() throws Exception {
long ts = System.currentTimeMillis();
String prefix = QUERY_TEST_PREFIX + "named_" + ts;
for (int i = 0; i < 4; i++) {
Device d = new Device();
d.setName(prefix + "_" + i);
d.setType("default");
client.saveDevice(d, null, null, null, null);
}
EntityDataQuery query = new EntityDataQuery()
.entityFilter(new EntityNameFilter()
.entityType(EntityType.DEVICE)
.entityNameFilter(prefix))
.pageLink(pageLink(10))
.addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
PageDataEntityData result = client.findEntityDataByQuery(query);
assertNotNull(result);
assertEquals(4, result.getTotalElements().intValue());
assertFalse(result.getHasNext());
}
@Test
public void testFindByEntityListFilter() throws Exception {
long ts = System.currentTimeMillis();
Device d1 = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_1").type("default"), null, null, null, null);
Device d2 = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_2").type("default"), null, null, null, null);
client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_3").type("default"), null, null, null, null);
EntityDataQuery query = new EntityDataQuery()
.entityFilter(new EntityListFilter()
.entityType(EntityType.DEVICE)
.entityList(List.of(
d1.getId().getId().toString(),
d2.getId().getId().toString())))
.pageLink(pageLink(10))
.addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
PageDataEntityData result = client.findEntityDataByQuery(query);
assertNotNull(result);
assertEquals(2, result.getTotalElements().intValue());
List<String> returnedIds = result.getData().stream()
.map(e -> e.getEntityId().getId().toString())
.collect(Collectors.toList());
assertTrue(returnedIds.contains(d1.getId().getId().toString()));
assertTrue(returnedIds.contains(d2.getId().getId().toString()));
}
@Test
public void testFindBySingleEntityFilter() throws Exception {
long ts = System.currentTimeMillis();
Device device = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "single_" + ts).type("default"), null, null, null, null);
EntityDataQuery query = new EntityDataQuery()
.entityFilter(new SingleEntityFilter()
.singleEntity(new AliasEntityId()
.id(device.getId().getId())
.entityType(EntityType.DEVICE)))
.pageLink(pageLink(10))
.addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
PageDataEntityData result = client.findEntityDataByQuery(query);
assertNotNull(result);
assertEquals(1, result.getTotalElements().intValue());
assertEquals(device.getId().getId().toString(),
result.getData().get(0).getEntityId().getId().toString());
}
@Test
public void testFindByAssetTypeFilter() throws Exception {
long ts = System.currentTimeMillis();
String assetType = "building";
for (int i = 0; i < 3; i++) {
Asset a = new Asset();
a.setName(QUERY_TEST_PREFIX + "asset_" + ts + "_" + i);
a.setType(assetType);
client.saveAsset(a, null, null, null);
}
EntityDataQuery query = new EntityDataQuery()
.entityFilter(new AssetTypeFilter()
.assetTypes(List.of(assetType)))
.pageLink(pageLink(10))
.addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
PageDataEntityData result = client.findEntityDataByQuery(query);
assertNotNull(result);
assertEquals(3, result.getTotalElements().intValue());
}
@Test
public void testFindWithKeyFilter() throws Exception {
long ts = System.currentTimeMillis();
String matchName = QUERY_TEST_PREFIX + "kf_match_" + ts;
String noMatchName = QUERY_TEST_PREFIX + "kf_other_" + ts;
client.saveDevice(new Device().name(matchName).type("default"), null, null, null, null);
client.saveDevice(new Device().name(noMatchName).type("default"), null, null, null, null);
KeyFilter nameKeyFilter = new KeyFilter()
.key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"))
.valueType(EntityKeyValueType.STRING)
.predicate(new StringFilterPredicate()
.operation(StringOperation.CONTAINS)
.value(new FilterPredicateValueString().defaultValue("kf_match"))
.ignoreCase(true));
EntityDataQuery query = new EntityDataQuery()
.entityFilter(new EntityNameFilter()
.entityType(EntityType.DEVICE)
.entityNameFilter(QUERY_TEST_PREFIX + "kf_"))
.addKeyFiltersItem(nameKeyFilter)
.pageLink(pageLink(10))
.addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
PageDataEntityData result = client.findEntityDataByQuery(query);
assertNotNull(result);
assertEquals(1, result.getTotalElements().intValue());
}
@Test
public void testFindWithPagination() throws Exception {
long ts = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
Device d = new Device();
d.setName(QUERY_TEST_PREFIX + "page_" + ts + "_" + i);
d.setType("default");
client.saveDevice(d, null, null, null, null);
}
EntityDataPageLink smallPage = new EntityDataPageLink()
.pageSize(2)
.page(0)
.sortOrder(new EntityDataSortOrder()
.key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"))
.direction(Direction.ASC));
EntityDataQuery query = new EntityDataQuery()
.entityFilter(new EntityNameFilter()
.entityType(EntityType.DEVICE)
.entityNameFilter(QUERY_TEST_PREFIX + "page_" + ts))
.pageLink(smallPage)
.addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name"));
// first page
PageDataEntityData page1 = client.findEntityDataByQuery(query);
assertNotNull(page1);
assertEquals(5, page1.getTotalElements().intValue());
assertEquals(3, page1.getTotalPages().intValue());
assertEquals(2, page1.getData().size());
assertTrue(page1.getHasNext());
// last page
smallPage.setPage(2);
PageDataEntityData lastPage = client.findEntityDataByQuery(query);
assertNotNull(lastPage);
assertEquals(1, lastPage.getData().size());
assertFalse(lastPage.getHasNext());
}
}

182
application/src/test/java/org/thingsboard/server/client/EntityRelationApiClientTest.java

@ -0,0 +1,182 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.Asset;
import org.thingsboard.client.model.Device;
import org.thingsboard.client.model.EntityRelation;
import org.thingsboard.client.model.EntityRelationInfo;
import org.thingsboard.client.model.EntityRelationsQuery;
import org.thingsboard.client.model.EntitySearchDirection;
import org.thingsboard.client.model.EntityType;
import org.thingsboard.client.model.RelationEntityTypeFilter;
import org.thingsboard.client.model.RelationTypeGroup;
import org.thingsboard.client.model.RelationsSearchParameters;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class EntityRelationApiClientTest extends AbstractApiClientTest {
@Test
public void testEntityRelationLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
// create assets and devices to relate
Asset building = new Asset();
building.setName(TEST_PREFIX + "Building_" + timestamp);
building.setType("building");
building = client.saveAsset(building, null, null, null);
Asset floor = new Asset();
floor.setName(TEST_PREFIX + "Floor_" + timestamp);
floor.setType("floor");
floor = client.saveAsset(floor, null, null, null);
Device device1 = new Device();
device1.setName(TEST_PREFIX + "Sensor_" + timestamp + "_1");
device1.setType("sensor");
device1 = client.saveDevice(device1, null, null, null, null);
Device device2 = new Device();
device2.setName(TEST_PREFIX + "Sensor_" + timestamp + "_2");
device2.setType("sensor");
device2 = client.saveDevice(device2, null, null, null, null);
Device device3 = new Device();
device3.setName(TEST_PREFIX + "Sensor_" + timestamp + "_3");
device3.setType("sensor");
device3 = client.saveDevice(device3, null, null, null, null);
// create relations: building -> Contains -> floor, floor -> Contains -> device1/device2/device3
EntityRelation buildingToFloor = new EntityRelation();
buildingToFloor.setFrom(building.getId());
buildingToFloor.setTo(floor.getId());
buildingToFloor.setType("Contains");
buildingToFloor.setTypeGroup(RelationTypeGroup.COMMON);
EntityRelation savedRelation = client.saveRelation(buildingToFloor);
assertNotNull(savedRelation);
assertEquals("Contains", savedRelation.getType());
client.saveRelation(new EntityRelation()
.from(floor.getId())
.to(device1.getId())
.type("Contains")
.typeGroup(RelationTypeGroup.COMMON));
client.saveRelation(new EntityRelation()
.from(floor.getId())
.to(device2.getId())
.type("Contains").typeGroup(RelationTypeGroup.COMMON));
client.saveRelation(new EntityRelation()
.from(floor.getId())
.to(device3.getId())
.type("Manages")
.typeGroup(RelationTypeGroup.COMMON));
// get specific relation
EntityRelation fetched = client.getRelation(
building.getId().getId().toString(), "ASSET",
"Contains",
floor.getId().getId().toString(), "ASSET",
RelationTypeGroup.COMMON.getValue());
assertNotNull(fetched);
assertEquals("Contains", fetched.getType());
// find all relations from floor
List<EntityRelation> fromFloor = client.findEntityRelationsByFrom("ASSET",
floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue());
assertEquals(3, fromFloor.size());
// find relations from floor with type filter "Contains"
List<EntityRelation> containsFromFloor = client.findEntityRelationsByFromAndRelationType("ASSET",
floor.getId().getId().toString(), "Contains", RelationTypeGroup.COMMON.getValue());
assertEquals(2, containsFromFloor.size());
// find relations to device1
List<EntityRelation> toDevice1 = client.findEntityRelationsByTo("DEVICE",
device1.getId().getId().toString(), RelationTypeGroup.COMMON.getValue());
assertEquals(1, toDevice1.size());
assertEquals("Contains", toDevice1.get(0).getType());
// find relations to device3 with type filter "Manages"
List<EntityRelation> managesToDevice3 = client.findEntityRelationsByToAndRelationType("DEVICE",
device3.getId().getId().toString(), "Manages", RelationTypeGroup.COMMON.getValue());
assertEquals(1, managesToDevice3.size());
// find info by from (includes entity names)
List<EntityRelationInfo> infoFromFloor = client.findEntityRelationInfosByFrom("ASSET",
floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue());
assertEquals(3, infoFromFloor.size());
Device finalDevice = device1;
assertTrue(infoFromFloor.stream().anyMatch(info ->
finalDevice.getName().equals(info.getToName())));
// find info by to
List<EntityRelationInfo> infoToDevice2 = client.findEntityRelationInfosByTo("DEVICE",
device2.getId().getId().toString(), RelationTypeGroup.COMMON.getValue());
assertEquals(1, infoToDevice2.size());
assertEquals(floor.getName(), infoToDevice2.get(0).getFromName());
// find by query - search from building, direction FROM, max 2 levels
RelationsSearchParameters params = new RelationsSearchParameters();
params.setRootId(building.getId().getId());
params.setRootType(EntityType.ASSET);
params.setDirection(EntitySearchDirection.FROM);
params.setRelationTypeGroup(RelationTypeGroup.COMMON);
params.setMaxLevel(2);
RelationEntityTypeFilter filter = new RelationEntityTypeFilter();
filter.setRelationType("Contains");
filter.setEntityTypes(List.of(EntityType.ASSET, EntityType.DEVICE));
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(params);
query.setFilters(List.of(filter));
List<EntityRelation> queryResult = client.findEntityRelationsByQuery(query);
assertTrue(queryResult.size() >= 3);
// find info by query
List<EntityRelationInfo> infoQueryResult = client.findEntityRelationInfosByQuery(query);
assertTrue(infoQueryResult.size() >= 3);
// delete single relation
client.deleteRelation(
floor.getId().getId().toString(), "ASSET",
"Manages",
device3.getId().getId().toString(), "DEVICE",
RelationTypeGroup.COMMON.getValue());
// verify deletion
List<EntityRelation> afterDelete = client.findEntityRelationsByFrom("ASSET",
floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue());
assertEquals(2, afterDelete.size());
// delete all relations for building
client.deleteRelations(building.getId().getId().toString(), "ASSET");
List<EntityRelation> afterDeleteAll = client.findEntityRelationsByFrom("ASSET",
building.getId().getId().toString(), RelationTypeGroup.COMMON.getValue());
assertEquals(0, afterDeleteAll.size());
}
}

267
application/src/test/java/org/thingsboard/server/client/EntityViewApiClientTest.java

@ -0,0 +1,267 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.AttributesEntityView;
import org.thingsboard.client.model.Device;
import org.thingsboard.client.model.EntitySubtype;
import org.thingsboard.client.model.EntityView;
import org.thingsboard.client.model.EntityViewInfo;
import org.thingsboard.client.model.PageDataEntityView;
import org.thingsboard.client.model.PageDataEntityViewInfo;
import org.thingsboard.client.model.TelemetryEntityView;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class EntityViewApiClientTest extends AbstractApiClientTest {
private static final String EV_PREFIX = "EvTest_";
@Test
public void testSaveAndGetEntityView() throws Exception {
long ts = System.currentTimeMillis();
Device device = createTestDevice(String.valueOf(ts));
EntityView ev = new EntityView();
ev.setName(EV_PREFIX + "save_" + ts);
ev.setType("testType");
ev.setEntityId(device.getId());
ev.setKeys(new TelemetryEntityView()
.timeseries(List.of("temperature", "humidity"))
.attributes(new AttributesEntityView()
.cs(List.of("firmware"))
.ss(List.of("active"))
.sh(List.of())));
ev.setStartTimeMs(1000L);
ev.setEndTimeMs(2000L);
EntityView saved = client.saveEntityView(ev, null, null, null);
assertNotNull(saved);
assertNotNull(saved.getId());
assertEquals(ev.getName(), saved.getName());
assertEquals("testType", saved.getType());
assertEquals(device.getId().getId(), saved.getEntityId().getId());
assertEquals(List.of("temperature", "humidity"), saved.getKeys().getTimeseries());
assertEquals(1000L, saved.getStartTimeMs().longValue());
assertEquals(2000L, saved.getEndTimeMs().longValue());
// get by id
String evId = saved.getId().getId().toString();
EntityView fetched = client.getEntityViewById(evId);
assertNotNull(fetched);
assertEquals(saved.getName(), fetched.getName());
assertEquals(saved.getType(), fetched.getType());
assertEquals(saved.getEntityId().getId(), fetched.getEntityId().getId());
}
@Test
public void testGetEntityViewInfoById() throws Exception {
long ts = System.currentTimeMillis();
Device device = createTestDevice(String.valueOf(ts));
EntityView saved = createEntityView(EV_PREFIX + "info_" + ts, "infoType", device);
EntityViewInfo info = client.getEntityViewInfoById(saved.getId().getId().toString());
assertNotNull(info);
assertEquals(saved.getName(), info.getName());
assertEquals("infoType", info.getType());
assertNotNull(info.getEntityId());
}
@Test
public void testUpdateEntityView() throws Exception {
long ts = System.currentTimeMillis();
Device device = createTestDevice(String.valueOf(ts));
EntityView saved = createEntityView(EV_PREFIX + "update_" + ts, "default", device);
saved.setName(EV_PREFIX + "updated_" + ts);
saved.setKeys(new TelemetryEntityView()
.timeseries(List.of("temperature", "pressure"))
.attributes(new AttributesEntityView()
.cs(List.of())
.ss(List.of())
.sh(List.of())));
EntityView updated = client.saveEntityView(saved, null, null, null);
assertEquals(EV_PREFIX + "updated_" + ts, updated.getName());
assertEquals(List.of("temperature", "pressure"), updated.getKeys().getTimeseries());
assertEquals(saved.getId().getId(), updated.getId().getId());
}
@Test
public void testDeleteEntityView() throws Exception {
long ts = System.currentTimeMillis();
Device device = createTestDevice(String.valueOf(ts));
EntityView saved = createEntityView(EV_PREFIX + "delete_" + ts, "default", device);
String evId = saved.getId().getId().toString();
client.getEntityViewById(evId);
client.deleteEntityView(evId);
assertReturns404(() -> client.getEntityViewById(evId));
}
@Test
public void testGetTenantEntityViews() throws Exception {
long ts = System.currentTimeMillis();
Device device = createTestDevice(String.valueOf(ts));
for (int i = 0; i < 3; i++) {
createEntityView(EV_PREFIX + "tenant_" + ts + "_" + i, "tenantViewType", device);
}
PageDataEntityView page = client.getTenantEntityViews(100, 0, null, EV_PREFIX + "tenant_" + ts, null, null);
assertNotNull(page);
assertEquals(3, page.getTotalElements().intValue());
for (EntityView ev : page.getData()) {
assertTrue(ev.getName().startsWith(EV_PREFIX + "tenant_" + ts));
}
}
@Test
public void testGetTenantEntityViewInfos() throws Exception {
long ts = System.currentTimeMillis();
Device device = createTestDevice(String.valueOf(ts));
createEntityView(EV_PREFIX + "tinfo_" + ts, "default", device);
PageDataEntityViewInfo page = client.getTenantEntityViewInfos(100, 0, null, EV_PREFIX + "tinfo_" + ts, null, null);
assertNotNull(page);
assertEquals(1, page.getTotalElements().intValue());
assertEquals(EV_PREFIX + "tinfo_" + ts, page.getData().get(0).getName());
}
@Test
public void testAssignAndUnassignEntityViewToCustomer() throws Exception {
long ts = System.currentTimeMillis();
Device device = createTestDevice(String.valueOf(ts));
EntityView saved = createEntityView(EV_PREFIX + "assign_" + ts, "default", device);
String evId = saved.getId().getId().toString();
String customerId = savedClientCustomer.getId().getId().toString();
// assign to customer
EntityView assigned = client.assignEntityViewToCustomer(customerId, evId);
assertNotNull(assigned);
assertEquals(savedClientCustomer.getId().getId(), assigned.getCustomerId().getId());
// verify in customer entity views
PageDataEntityView customerViews = client.getCustomerEntityViews(
customerId, 100, 0, null, EV_PREFIX + "assign_" + ts, null, null);
assertEquals(1, customerViews.getTotalElements().intValue());
assertEquals(saved.getName(), customerViews.getData().get(0).getName());
// unassign from customer
EntityView unassigned = client.unassignEntityViewFromCustomer(evId);
assertNotNull(unassigned);
PageDataEntityView afterUnassign = client.getCustomerEntityViews(
customerId, 100, 0, null, EV_PREFIX + "assign_" + ts, null, null);
assertEquals(0, afterUnassign.getTotalElements().intValue());
}
@Test
public void testGetCustomerEntityViewInfos() throws Exception {
long ts = System.currentTimeMillis();
Device device = createTestDevice(String.valueOf(ts));
EntityView saved = createEntityView(EV_PREFIX + "cinfo_" + ts, "default", device);
String evId = saved.getId().getId().toString();
String customerId = savedClientCustomer.getId().getId().toString();
client.assignEntityViewToCustomer(customerId, evId);
PageDataEntityViewInfo infos = client.getCustomerEntityViewInfos(
customerId, 100, 0, null, EV_PREFIX + "cinfo_" + ts, null, null);
assertNotNull(infos);
assertEquals(1, infos.getTotalElements().intValue());
assertEquals(saved.getName(), infos.getData().get(0).getName());
}
@Test
public void testGetEntityViewTypes() throws Exception {
long ts = System.currentTimeMillis();
Device device = createTestDevice(String.valueOf(ts));
createEntityView(EV_PREFIX + "types_" + ts, "uniqueEvType_" + ts, device);
List<EntitySubtype> types = client.getEntityViewTypes();
assertNotNull(types);
assertFalse(types.isEmpty());
List<String> typeNames = types.stream()
.map(EntitySubtype::getType)
.collect(Collectors.toList());
assertTrue(typeNames.contains("uniqueEvType_" + ts));
}
@Test
public void testGetEntityViewById_notFound() {
String nonExistentId = UUID.randomUUID().toString();
assertReturns404(() -> client.getEntityViewById(nonExistentId));
}
@Test
public void testGetTenantEntityViewsPagination() throws Exception {
long ts = System.currentTimeMillis();
Device device = createTestDevice(String.valueOf(ts));
for (int i = 0; i < 5; i++) {
createEntityView(EV_PREFIX + "paged_" + ts + "_" + i, "default", device);
}
PageDataEntityView page1 = client.getTenantEntityViews(2, 0, null, EV_PREFIX + "paged_" + ts, null, null);
assertNotNull(page1);
assertEquals(5, page1.getTotalElements().intValue());
assertEquals(3, page1.getTotalPages().intValue());
assertEquals(2, page1.getData().size());
assertTrue(page1.getHasNext());
PageDataEntityView lastPage = client.getTenantEntityViews(2, 2, null, EV_PREFIX + "paged_" + ts, null, null);
assertEquals(1, lastPage.getData().size());
assertFalse(lastPage.getHasNext());
}
private Device createTestDevice(String suffix) throws Exception {
Device device = new Device();
device.setName(EV_PREFIX + "device_" + suffix);
device.setType("default");
return client.saveDevice(device, null, null, null, null);
}
private EntityView createEntityView(String name, String type, Device device) throws Exception {
EntityView ev = new EntityView();
ev.setName(name);
ev.setType(type);
ev.setEntityId(device.getId());
ev.setKeys(new TelemetryEntityView()
.timeseries(List.of("temperature"))
.attributes(new AttributesEntityView()
.cs(List.of())
.ss(List.of())
.sh(List.of())));
return client.saveEntityView(ev, null, null, null);
}
}

156
application/src/test/java/org/thingsboard/server/client/MobileAppApiClientTest.java

@ -0,0 +1,156 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.MobileApp;
import org.thingsboard.client.model.MobileAppBundle;
import org.thingsboard.client.model.MobileAppBundleInfo;
import org.thingsboard.client.model.MobileAppStatus;
import org.thingsboard.client.model.PageDataMobileApp;
import org.thingsboard.client.model.PageDataMobileAppBundleInfo;
import org.thingsboard.client.model.PlatformType;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@DaoSqlTest
public class MobileAppApiClientTest extends AbstractApiClientTest {
@Test
public void testMobileAppLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<MobileApp> createdApps = new ArrayList<>();
// create 3 Android apps
for (int i = 0; i < 3; i++) {
MobileApp app = new MobileApp();
app.setPkgName("com.test.android." + timestamp + "." + i);
app.setTitle(TEST_PREFIX + "AndroidApp_" + timestamp + "_" + i);
app.setAppSecret("secret_android_" + timestamp + "_" + i);
app.setPlatformType(PlatformType.ANDROID);
app.setStatus(MobileAppStatus.DRAFT);
MobileApp created = client.saveMobileApp(app);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals(app.getPkgName(), created.getPkgName());
assertEquals(PlatformType.ANDROID, created.getPlatformType());
assertEquals(MobileAppStatus.DRAFT, created.getStatus());
createdApps.add(created);
}
// create 2 iOS apps
for (int i = 0; i < 2; i++) {
MobileApp app = new MobileApp();
app.setPkgName("com.test.ios." + timestamp + "." + i);
app.setTitle(TEST_PREFIX + "IosApp_" + timestamp + "_" + i);
app.setAppSecret("secret_ios_" + timestamp + "_" + i);
app.setPlatformType(PlatformType.IOS);
app.setStatus(MobileAppStatus.DRAFT);
MobileApp created = client.saveMobileApp(app);
assertNotNull(created);
createdApps.add(created);
}
// list all tenant mobile apps
PageDataMobileApp allApps = client.getTenantMobileApps(100, 0, null,
null, null, null);
assertNotNull(allApps);
assertEquals(5, allApps.getData().size());
// list with platform type filter
PageDataMobileApp androidApps = client.getTenantMobileApps(100, 0, PlatformType.ANDROID,
null, null, null);
assertEquals(3, androidApps.getData().size());
PageDataMobileApp iosApps = client.getTenantMobileApps(100, 0, PlatformType.IOS,
null, null, null);
assertEquals(2, iosApps.getData().size());
// get mobile app by id
MobileApp searchApp = createdApps.get(1);
MobileApp fetchedApp = client.getMobileAppById(searchApp.getId().getId());
assertEquals(searchApp.getPkgName(), fetchedApp.getPkgName());
assertEquals(searchApp.getTitle(), fetchedApp.getTitle());
assertEquals(searchApp.getPlatformType(), fetchedApp.getPlatformType());
// update mobile app
MobileApp appToUpdate = createdApps.get(2);
appToUpdate.setTitle(appToUpdate.getTitle() + "_updated");
MobileApp updatedApp = client.saveMobileApp(appToUpdate);
assertEquals(appToUpdate.getTitle(), updatedApp.getTitle());
// create mobile app bundle with android and ios apps
MobileAppBundle bundle = new MobileAppBundle();
bundle.setTitle(TEST_PREFIX + "Bundle_" + timestamp);
bundle.setDescription("Test bundle");
bundle.setAndroidAppId(createdApps.get(0).getId());
bundle.setIosAppId(createdApps.get(3).getId());
bundle.setOauth2Enabled(false);
MobileAppBundle savedBundle = client.saveMobileAppBundle(bundle, null);
assertNotNull(savedBundle);
assertNotNull(savedBundle.getId());
assertEquals(bundle.getTitle(), savedBundle.getTitle());
// get bundle info by id
MobileAppBundleInfo bundleInfo = client.getMobileAppBundleInfoById(savedBundle.getId().getId());
assertEquals(savedBundle.getTitle(), bundleInfo.getTitle());
assertEquals("Test bundle", bundleInfo.getDescription());
assertNotNull(bundleInfo.getAndroidPkgName());
assertNotNull(bundleInfo.getIosPkgName());
// list tenant bundles
PageDataMobileAppBundleInfo bundles = client.getTenantMobileAppBundleInfos(100, 0,
TEST_PREFIX + "Bundle_" + timestamp, null, null);
assertEquals(1, bundles.getData().size());
// update bundle
savedBundle.setDescription("Updated description");
MobileAppBundle updatedBundle = client.saveMobileAppBundle(savedBundle, null);
assertEquals("Updated description", updatedBundle.getDescription());
// delete bundle
client.deleteMobileAppBundle(savedBundle.getId().getId());
// verify bundle deletion
assertReturns404(() ->
client.getMobileAppBundleInfoById(savedBundle.getId().getId())
);
// delete mobile app
UUID appToDeleteId = createdApps.get(0).getId().getId();
client.deleteMobileApp(appToDeleteId);
// verify app deletion
assertReturns404(() ->
client.getMobileAppById(appToDeleteId)
);
PageDataMobileApp appsAfterDelete = client.getTenantMobileApps(100, 0, null,
null, null, null);
assertEquals(4, appsAfterDelete.getData().size());
}
}

278
application/src/test/java/org/thingsboard/server/client/NotificationApiClientTest.java

@ -0,0 +1,278 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.client.model.EntityActionRecipientsConfig;
import org.thingsboard.client.model.EntityType;
import org.thingsboard.client.model.NotificationDeliveryMethod;
import org.thingsboard.client.model.NotificationRequest;
import org.thingsboard.client.model.NotificationRequestInfo;
import org.thingsboard.client.model.NotificationRule;
import org.thingsboard.client.model.NotificationRuleInfo;
import org.thingsboard.client.model.NotificationRuleTriggerType;
import org.thingsboard.client.model.NotificationSettings;
import org.thingsboard.client.model.NotificationTarget;
import org.thingsboard.client.model.NotificationTemplate;
import org.thingsboard.client.model.NotificationTemplateConfig;
import org.thingsboard.client.model.NotificationType;
import org.thingsboard.client.model.PageDataNotification;
import org.thingsboard.client.model.PageDataNotificationRequestInfo;
import org.thingsboard.client.model.PageDataNotificationRuleInfo;
import org.thingsboard.client.model.PageDataNotificationTarget;
import org.thingsboard.client.model.PageDataNotificationTemplate;
import org.thingsboard.client.model.PlatformUsersNotificationTargetConfig;
import org.thingsboard.client.model.TenantAdministratorsFilter;
import org.thingsboard.client.model.WebDeliveryMethodNotificationTemplate;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class NotificationApiClientTest extends AbstractApiClientTest {
@Test
public void testNotificationLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
// === 1. Notification Target CRUD ===
// Create target
TenantAdministratorsFilter usersFilter = new TenantAdministratorsFilter();
PlatformUsersNotificationTargetConfig targetConfig =
new PlatformUsersNotificationTargetConfig().usersFilter(usersFilter);
NotificationTarget target =
new NotificationTarget()
.name("Test Target " + timestamp)
._configuration(targetConfig);
NotificationTarget savedTarget = client.saveNotificationTarget(target);
assertNotNull(savedTarget);
assertNotNull(savedTarget.getId());
assertEquals("Test Target " + timestamp, savedTarget.getName());
// Get target by ID
NotificationTarget fetchedTarget =
client.getNotificationTargetById(savedTarget.getId().getId());
assertEquals(savedTarget.getName(), fetchedTarget.getName());
// List targets
PageDataNotificationTarget targetsPage =
client.getNotificationTargets(100, 0, null, null, null);
assertNotNull(targetsPage);
assertNotNull(targetsPage.getData());
assertTrue(
targetsPage.getData().stream()
.anyMatch(t -> t.getName().equals(savedTarget.getName())));
// Update target
savedTarget.setName("Updated Target " + timestamp);
NotificationTarget updatedTarget = client.saveNotificationTarget(savedTarget);
assertEquals("Updated Target " + timestamp, updatedTarget.getName());
// === 2. Notification Template CRUD ===
// Create template
WebDeliveryMethodNotificationTemplate webTemplate =
new WebDeliveryMethodNotificationTemplate()
.subject("Test Subject")
.body("Test notification body")
.enabled(true);
NotificationTemplateConfig templateConfig =
new NotificationTemplateConfig()
.putDeliveryMethodsTemplatesItem("WEB", webTemplate);
NotificationTemplate template =
new NotificationTemplate()
.name("Test Template " + timestamp)
.notificationType(NotificationType.GENERAL)
._configuration(templateConfig);
NotificationTemplate savedTemplate = client.saveNotificationTemplate(template);
assertNotNull(savedTemplate);
assertNotNull(savedTemplate.getId());
assertEquals("Test Template " + timestamp, savedTemplate.getName());
// Get template by ID
NotificationTemplate fetchedTemplate =
client.getNotificationTemplateById(savedTemplate.getId().getId());
assertEquals(savedTemplate.getName(), fetchedTemplate.getName());
assertEquals(NotificationType.GENERAL, fetchedTemplate.getNotificationType());
// List templates
PageDataNotificationTemplate templatesPage =
client.getNotificationTemplates(100, 0, null, null, null, null);
assertNotNull(templatesPage);
assertTrue(
templatesPage.getData().stream()
.anyMatch(t -> t.getName().equals(savedTemplate.getName())));
// Update template
savedTemplate.setName("Updated Template " + timestamp);
NotificationTemplate updatedTemplate = client.saveNotificationTemplate(savedTemplate);
assertEquals("Updated Template " + timestamp, updatedTemplate.getName());
// === 3. Send notification & read notifications ===
// Send notification request
NotificationRequest request =
new NotificationRequest()
.targets(List.of(savedTarget.getId().getId()))
.templateId(savedTemplate.getId());
NotificationRequest sentRequest = client.createNotificationRequest(request);
assertNotNull(sentRequest);
assertNotNull(sentRequest.getId());
// Get request by ID
NotificationRequestInfo fetchedRequest =
client.getNotificationRequestById(sentRequest.getId().getId());
assertNotNull(fetchedRequest);
// List requests
PageDataNotificationRequestInfo requestsPage =
client.getNotificationRequests(100, 0, null, null, null);
assertNotNull(requestsPage);
assertFalse(requestsPage.getData().isEmpty());
// Get notifications for current user
PageDataNotification notificationsPage =
client.getNotifications(100, 0, null, null, null, null, null);
assertNotNull(notificationsPage);
assertFalse(notificationsPage.getData().isEmpty());
// Get unread count
Integer unreadCount = client.getUnreadNotificationsCount("WEB");
assertNotNull(unreadCount);
assertTrue("Expected at least one unread notification", unreadCount > 0);
// Mark single notification as read
client.markNotificationAsRead(
notificationsPage.getData().get(0).getId().getId());
// Mark all as read
client.markAllNotificationsAsRead(null);
Integer unreadAfterMarkAll = client.getUnreadNotificationsCount(null);
assertEquals("Expected no unread notifications after marking all as read", 0, unreadAfterMarkAll.intValue());
// === 4. Notification Settings ===
NotificationSettings settings = client.getNotificationSettings();
assertNotNull(settings);
List<NotificationDeliveryMethod> deliveryMethods = client.getAvailableDeliveryMethods();
assertNotNull(deliveryMethods);
assertTrue(deliveryMethods.contains(NotificationDeliveryMethod.WEB));
// === 5. Cleanup ===
// Delete notification request
client.deleteNotificationRequest(sentRequest.getId().getId());
assertReturns404(() -> client.getNotificationRequestById(sentRequest.getId().getId()));
// Delete template
client.deleteNotificationTemplateById(savedTemplate.getId().getId());
assertReturns404(() -> client.getNotificationTemplateById(savedTemplate.getId().getId()));
// Delete target
client.deleteNotificationTargetById(savedTarget.getId().getId());
assertReturns404(() -> client.getNotificationTargetById(savedTarget.getId().getId()));
}
@Test
public void testNotificationRuleLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
// Create a target for the rule recipients
TenantAdministratorsFilter usersFilter = new TenantAdministratorsFilter();
PlatformUsersNotificationTargetConfig targetConfig =
new PlatformUsersNotificationTargetConfig().usersFilter(usersFilter);
NotificationTarget target =
new NotificationTarget()
.name("Rule Test Target " + timestamp)
._configuration(targetConfig);
NotificationTarget savedTarget = client.saveNotificationTarget(target);
// Create a template of type ENTITY_ACTION
WebDeliveryMethodNotificationTemplate webTemplate =
new WebDeliveryMethodNotificationTemplate()
.subject("Entity action: ${entityType}")
.body("Entity ${entityName} was ${actionType}")
.enabled(true);
NotificationTemplateConfig templateConfig =
new NotificationTemplateConfig()
.putDeliveryMethodsTemplatesItem("WEB", webTemplate);
NotificationTemplate template =
new NotificationTemplate()
.name("Rule Test Template " + timestamp)
.notificationType(NotificationType.ENTITY_ACTION)
._configuration(templateConfig);
NotificationTemplate savedTemplate = client.saveNotificationTemplate(template);
// Build trigger config: fire on DEVICE create/update
EntityActionNotificationRuleTriggerConfig triggerConfig =
new EntityActionNotificationRuleTriggerConfig()
.addEntityTypesItem(EntityType.DEVICE)
.created(true)
.updated(true)
.deleted(false);
// Build recipients config
EntityActionRecipientsConfig recipientsConfig = new EntityActionRecipientsConfig()
.addTargetsItem(savedTarget.getId().getId());
// saveNotificationRule - create
NotificationRule rule = new NotificationRule()
.name("Test Rule " + timestamp)
.enabled(true)
.templateId(savedTemplate.getId())
.triggerType(NotificationRuleTriggerType.ENTITY_ACTION)
.triggerConfig(triggerConfig)
.recipientsConfig(recipientsConfig);
NotificationRule savedRule = client.saveNotificationRule(rule);
assertNotNull(savedRule);
assertNotNull(savedRule.getId());
assertEquals("Test Rule " + timestamp, savedRule.getName());
assertEquals(NotificationRuleTriggerType.ENTITY_ACTION, savedRule.getTriggerType());
assertEquals(Boolean.TRUE, savedRule.getEnabled());
// getNotificationRuleById
NotificationRuleInfo fetchedRule = client.getNotificationRuleById(savedRule.getId().getId());
assertNotNull(fetchedRule);
assertEquals(savedRule.getName(), fetchedRule.getName());
assertEquals(NotificationRuleTriggerType.ENTITY_ACTION, fetchedRule.getTriggerType());
// getNotificationRules - verify it appears in the list
PageDataNotificationRuleInfo rulesPage = client.getNotificationRules(100, 0, null, null, null);
assertNotNull(rulesPage);
assertTrue(rulesPage.getData().stream()
.anyMatch(r -> r.getId().getId().equals(savedRule.getId().getId())));
// deleteNotificationRule
client.deleteNotificationRule(savedRule.getId().getId());
assertReturns404(() -> client.getNotificationRuleById(savedRule.getId().getId()));
// Cleanup
client.deleteNotificationTemplateById(savedTemplate.getId().getId());
client.deleteNotificationTargetById(savedTarget.getId().getId());
}
}

138
application/src/test/java/org/thingsboard/server/client/Oauth2ApiClientTest.java

@ -0,0 +1,138 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.MapperType;
import org.thingsboard.client.model.OAuth2BasicMapperConfig;
import org.thingsboard.client.model.OAuth2Client;
import org.thingsboard.client.model.OAuth2ClientInfo;
import org.thingsboard.client.model.OAuth2MapperConfig;
import org.thingsboard.client.model.PageDataOAuth2ClientInfo;
import org.thingsboard.client.model.PlatformType;
import org.thingsboard.client.model.TenantNameStrategyType;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@DaoSqlTest
public class Oauth2ApiClientTest extends AbstractApiClientTest {
private OAuth2Client createOAuth2Client(String title, String clientId, String clientSecret) {
OAuth2BasicMapperConfig basicConfig = new OAuth2BasicMapperConfig();
basicConfig.setEmailAttributeKey("email");
basicConfig.setFirstNameAttributeKey("given_name");
basicConfig.setLastNameAttributeKey("family_name");
basicConfig.setTenantNameStrategy(TenantNameStrategyType.DOMAIN);
OAuth2MapperConfig mapperConfig = new OAuth2MapperConfig();
mapperConfig.setType(MapperType.BASIC);
mapperConfig.setAllowUserCreation(true);
mapperConfig.setActivateUser(false);
mapperConfig.setBasic(basicConfig);
OAuth2Client oAuth2Client = new OAuth2Client();
oAuth2Client.setTitle(title);
oAuth2Client.setClientId(clientId);
oAuth2Client.setClientSecret(clientSecret);
oAuth2Client.setAuthorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
oAuth2Client.setAccessTokenUri("https://oauth2.googleapis.com/token");
oAuth2Client.setScope(List.of("openid", "email", "profile"));
oAuth2Client.setUserInfoUri("https://openidconnect.googleapis.com/v1/userinfo");
oAuth2Client.setUserNameAttributeName("email");
oAuth2Client.setClientAuthenticationMethod("POST");
oAuth2Client.setLoginButtonLabel(title);
oAuth2Client.setMapperConfig(mapperConfig);
oAuth2Client.setPlatforms(List.of(PlatformType.WEB));
return oAuth2Client;
}
@Test
public void testOAuth2ClientLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<OAuth2Client> createdClients = new ArrayList<>();
// create 5 OAuth2 clients
for (int i = 0; i < 5; i++) {
String title = TEST_PREFIX + "OAuth2_" + timestamp + "_" + i;
OAuth2Client oAuth2Client = createOAuth2Client(title,
"client_id_" + timestamp + "_" + i,
"client_secret_" + timestamp + "_" + i);
OAuth2Client created = client.saveOAuth2Client(oAuth2Client);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals(title, created.getTitle());
assertEquals("POST", created.getClientAuthenticationMethod());
assertNotNull(created.getMapperConfig());
assertEquals(MapperType.BASIC, created.getMapperConfig().getType());
createdClients.add(created);
}
// list tenant OAuth2 client infos
PageDataOAuth2ClientInfo clientInfos = client.findTenantOAuth2ClientInfos(100, 0,
TEST_PREFIX + "OAuth2_" + timestamp, null, null);
assertNotNull(clientInfos);
assertEquals(5, clientInfos.getData().size());
// get OAuth2 client by id
OAuth2Client searchClient = createdClients.get(2);
OAuth2Client fetchedClient = client.getOAuth2ClientById(searchClient.getId().getId());
assertEquals(searchClient.getTitle(), fetchedClient.getTitle());
assertEquals(searchClient.getClientId(), fetchedClient.getClientId());
assertEquals(searchClient.getAuthorizationUri(), fetchedClient.getAuthorizationUri());
assertEquals(3, fetchedClient.getScope().size());
// fetch client infos by ids
List<String> idsToFetch = List.of(
createdClients.get(0).getId().getId().toString(),
createdClients.get(1).getId().getId().toString()
);
List<OAuth2ClientInfo> fetchedInfos = client.findTenantOAuth2ClientInfosByIds(idsToFetch);
assertEquals(2, fetchedInfos.size());
// update OAuth2 client
OAuth2Client clientToUpdate = client.getOAuth2ClientById(createdClients.get(3).getId().getId());
clientToUpdate.setTitle(clientToUpdate.getTitle() + "_updated");
clientToUpdate.setLoginButtonLabel("Updated Login");
clientToUpdate.setPlatforms(List.of(PlatformType.WEB, PlatformType.ANDROID));
OAuth2Client updatedClient = client.saveOAuth2Client(clientToUpdate);
assertEquals(clientToUpdate.getTitle(), updatedClient.getTitle());
assertEquals("Updated Login", updatedClient.getLoginButtonLabel());
assertEquals(2, updatedClient.getPlatforms().size());
// delete OAuth2 client
UUID clientToDeleteId = createdClients.get(0).getId().getId();
client.deleteOauth2Client(clientToDeleteId);
// verify deletion
assertReturns404(() ->
client.getOAuth2ClientById(clientToDeleteId)
);
PageDataOAuth2ClientInfo clientsAfterDelete = client.findTenantOAuth2ClientInfos(100, 0,
TEST_PREFIX + "OAuth2_" + timestamp, null, null);
assertEquals(4, clientsAfterDelete.getData().size());
}
}

261
application/src/test/java/org/thingsboard/server/client/OtaPackageApiClientTest.java

@ -0,0 +1,261 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.ChecksumAlgorithm;
import org.thingsboard.client.model.DeviceProfileId;
import org.thingsboard.client.model.DeviceProfileInfo;
import org.thingsboard.client.model.OtaPackage;
import org.thingsboard.client.model.OtaPackageInfo;
import org.thingsboard.client.model.OtaPackageType;
import org.thingsboard.client.model.PageDataOtaPackageInfo;
import org.thingsboard.client.model.SaveOtaPackageInfoRequest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.io.File;
import java.io.FileWriter;
import java.nio.file.Files;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class OtaPackageApiClientTest extends AbstractApiClientTest {
private static final String OTA_PREFIX = "OtaTest_";
private DeviceProfileId getDefaultDeviceProfileId() throws Exception {
DeviceProfileInfo profileInfo = client.getDefaultDeviceProfileInfo();
return (DeviceProfileId) profileInfo.getId();
}
private SaveOtaPackageInfoRequest buildOtaPackageInfoRequest(
String title, String version, OtaPackageType type,
DeviceProfileId deviceProfileId, boolean usesUrl, String url) {
SaveOtaPackageInfoRequest request = new SaveOtaPackageInfoRequest();
request.setTitle(title);
request.setType(type);
request.setUrl(url);
request.setVersion(version);
request.setDeviceProfileId(deviceProfileId);
return request;
}
private OtaPackageInfo createFirmwareInfo(String suffix) throws Exception {
DeviceProfileId profileId = getDefaultDeviceProfileId();
SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest(
OTA_PREFIX + suffix, "1.0." + System.currentTimeMillis(),
OtaPackageType.FIRMWARE, profileId, false, null);
return client.saveOtaPackageInfo(request);
}
private OtaPackageInfo createFirmwareWithUrl(String suffix) throws Exception {
DeviceProfileId profileId = getDefaultDeviceProfileId();
SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest(
OTA_PREFIX + suffix, "1.0." + System.currentTimeMillis(),
OtaPackageType.FIRMWARE, profileId, true, "https://example.com/firmware.bin");
return client.saveOtaPackageInfo(request);
}
@Test
public void testSaveAndGetOtaPackageInfo() throws Exception {
long ts = System.currentTimeMillis();
DeviceProfileId profileId = getDefaultDeviceProfileId();
String title = OTA_PREFIX + "save_" + ts;
String version = "1.0." + ts;
SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest(
title, version, OtaPackageType.FIRMWARE, profileId, true, "https://example.com/fw.bin");
OtaPackageInfo saved = client.saveOtaPackageInfo(request);
assertNotNull(saved);
assertNotNull(saved.getId());
assertEquals(title, saved.getTitle());
assertEquals(version, saved.getVersion());
assertEquals(OtaPackageType.FIRMWARE, saved.getType());
assertTrue(saved.getUrl().contains("example.com"));
// get info by id
String pkgId = saved.getId().getId().toString();
OtaPackageInfo fetched = client.getOtaPackageInfoById(pkgId);
assertNotNull(fetched);
assertEquals(title, fetched.getTitle());
assertEquals(version, fetched.getVersion());
}
@Test
public void testGetOtaPackageById() throws Exception {
long ts = System.currentTimeMillis();
OtaPackageInfo saved = createFirmwareWithUrl("getbyid_" + ts);
OtaPackage fullPkg = client.getOtaPackageById(saved.getId().getId().toString());
assertNotNull(fullPkg);
assertEquals(saved.getTitle(), fullPkg.getTitle());
assertEquals(saved.getVersion(), fullPkg.getVersion());
}
@Test
public void testSaveOtaPackageInfoForSoftware() throws Exception {
long ts = System.currentTimeMillis();
DeviceProfileId profileId = getDefaultDeviceProfileId();
String title = OTA_PREFIX + "sw_" + ts;
SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest(
title, "2.0." + ts, OtaPackageType.SOFTWARE, profileId, true, "https://example.com/sw.bin");
OtaPackageInfo saved = client.saveOtaPackageInfo(request);
assertNotNull(saved);
assertEquals(OtaPackageType.SOFTWARE, saved.getType());
assertEquals(title, saved.getTitle());
}
@Test
public void testSaveOtaPackageData() throws Exception {
long ts = System.currentTimeMillis();
OtaPackageInfo info = createFirmwareInfo("data_" + ts);
File tempFile = Files.createTempFile("ota_test_", ".bin").toFile();
tempFile.deleteOnExit();
try (FileWriter writer = new FileWriter(tempFile)) {
writer.write("test firmware content " + ts);
}
OtaPackageInfo updated = client.saveOtaPackageData(
info.getId().getId().toString(), "MD5", tempFile, null);
assertNotNull(updated);
assertTrue(updated.getHasData());
assertNotNull(updated.getFileName());
assertNotNull(updated.getDataSize());
assertTrue(updated.getDataSize() > 0);
assertEquals(ChecksumAlgorithm.MD5, updated.getChecksumAlgorithm());
}
@Test
public void testDownloadOtaPackage() throws Exception {
long ts = System.currentTimeMillis();
OtaPackageInfo info = createFirmwareInfo("download_" + ts);
String content = "downloadable firmware " + ts;
File tempFile = Files.createTempFile("ota_dl_", ".bin").toFile();
tempFile.deleteOnExit();
try (FileWriter writer = new FileWriter(tempFile)) {
writer.write(content);
}
client.saveOtaPackageData(info.getId().getId().toString(), "MD5", tempFile, null);
File downloaded = client.downloadOtaPackage(info.getId().getId().toString());
assertNotNull(downloaded);
assertTrue(downloaded.length() > 0);
String downloadedContent = Files.readString(downloaded.toPath());
assertEquals(content, downloadedContent);
}
@Test
public void testDeleteOtaPackage() throws Exception {
long ts = System.currentTimeMillis();
OtaPackageInfo saved = createFirmwareWithUrl("delete_" + ts);
String pkgId = saved.getId().getId().toString();
client.getOtaPackageInfoById(pkgId);
client.deleteOtaPackage(pkgId);
assertReturns404(() -> client.getOtaPackageInfoById(pkgId));
}
@Test
public void testGetOtaPackages() throws Exception {
long ts = System.currentTimeMillis();
for (int i = 0; i < 3; i++) {
createFirmwareWithUrl("list_" + ts + "_" + i);
}
PageDataOtaPackageInfo page = client.getOtaPackages(100, 0, OTA_PREFIX + "list_" + ts, null, null);
assertNotNull(page);
assertEquals(3, page.getTotalElements().intValue());
for (OtaPackageInfo pkg : page.getData()) {
assertTrue(pkg.getTitle().startsWith(OTA_PREFIX + "list_" + ts));
}
}
@Test
public void testGetOtaPackagesByDeviceProfileAndType() throws Exception {
long ts = System.currentTimeMillis();
DeviceProfileId profileId = getDefaultDeviceProfileId();
createFirmwareWithUrl("byprofile_" + ts + "_0");
createFirmwareWithUrl("byprofile_" + ts + "_1");
PageDataOtaPackageInfo page = client.getOtaPackagesByDeviceProfileAndType(
profileId.getId().toString(), "FIRMWARE", 100, 0,
OTA_PREFIX + "byprofile_" + ts, null, null);
assertNotNull(page);
assertEquals(2, page.getTotalElements().intValue());
}
@Test
public void testGetOtaPackageInfoById_notFound() {
String nonExistentId = UUID.randomUUID().toString();
assertReturns404(() -> client.getOtaPackageInfoById(nonExistentId));
}
@Test
public void testGetOtaPackagesPagination() throws Exception {
long ts = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
createFirmwareWithUrl("paged_" + ts + "_" + i);
}
PageDataOtaPackageInfo page1 = client.getOtaPackages(2, 0, OTA_PREFIX + "paged_" + ts, null, null);
assertNotNull(page1);
assertEquals(5, page1.getTotalElements().intValue());
assertEquals(3, page1.getTotalPages().intValue());
assertEquals(2, page1.getData().size());
assertTrue(page1.getHasNext());
PageDataOtaPackageInfo lastPage = client.getOtaPackages(2, 2, OTA_PREFIX + "paged_" + ts, null, null);
assertEquals(1, lastPage.getData().size());
assertFalse(lastPage.getHasNext());
}
@Test
public void testUpdateOtaPackageInfo() throws Exception {
long ts = System.currentTimeMillis();
OtaPackageInfo saved = createFirmwareWithUrl("update_" + ts);
SaveOtaPackageInfoRequest updateReq = new SaveOtaPackageInfoRequest();
updateReq.setId(saved.getId());
updateReq.setTitle(saved.getTitle());
updateReq.setType(saved.getType());
updateReq.setVersion(saved.getVersion());
updateReq.setDeviceProfileId(saved.getDeviceProfileId());
updateReq.setUrl(saved.getUrl());
updateReq.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("infoKey", "infoValue"));
OtaPackageInfo updated = client.saveOtaPackageInfo(updateReq);
assertNotNull(updated);
assertEquals(saved.getId().getId(), updated.getId().getId());
assertEquals("infoValue", updated.getAdditionalInfo().get("infoKey").asText());
}
}

72
application/src/test/java/org/thingsboard/server/client/RpcV1ApiClientTest.java

@ -0,0 +1,72 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.ApiException;
import org.thingsboard.client.model.Device;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.junit.Assert.assertEquals;
@DaoSqlTest
public class RpcV1ApiClientTest extends AbstractApiClientTest {
private static final String ONE_WAY_BODY =
"{\"method\":\"setGpio\",\"params\":{\"pin\":7,\"value\":1},\"persistent\":true}";
private static final String TWO_WAY_BODY =
"{\"method\":\"getGpio\",\"params\":{\"pin\":7},\"persistent\":true}";
@Test
public void testHandleOneWayDeviceRPCRequest() throws Exception {
long ts = System.currentTimeMillis();
Device device = createNewDevice(TEST_PREFIX + ts);
String deviceId = device.getId().getId().toString();
try {
client.handleOneWayDeviceRPCRequestV1(deviceId, ONE_WAY_BODY);
} catch (ApiException e) {
assertEquals("handleOneWayDeviceRPCRequest got an unexpected HTTP error: " + e.getCode(),
0, e.getCode());
}
client.deleteDevice(deviceId);
}
@Test
public void testHandleTwoWayDeviceRPCRequest() throws Exception {
long ts = System.currentTimeMillis();
Device device = createNewDevice(TEST_PREFIX + ts);
String deviceId = device.getId().getId().toString();
try {
client.handleTwoWayDeviceRPCRequestV1(deviceId, TWO_WAY_BODY);
} catch (ApiException e) {
assertEquals("handleTwoWayDeviceRPCRequest got an unexpected HTTP error: " + e.getCode(),
0, e.getCode());
}
client.deleteDevice(deviceId);
}
private Device createNewDevice(String name) throws ApiException {
Device device = new Device();
device.setName(name);
device.setType("default");
return client.saveDevice(device, null, null, null, null);
}
}

133
application/src/test/java/org/thingsboard/server/client/RpcV2ApiClientTest.java

@ -0,0 +1,133 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.ApiException;
import org.thingsboard.client.model.Device;
import org.thingsboard.client.model.Rpc;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@DaoSqlTest
public class RpcV2ApiClientTest extends AbstractApiClientTest {
private static final String PERSISTENT_BODY =
"{\"method\":\"setGpio\",\"params\":{\"pin\":7,\"value\":1},\"persistent\":true}";
@Test
public void testHandleOneWayDeviceRPCRequest() throws Exception {
long ts = System.currentTimeMillis();
Device device = createNewDevice(TEST_PREFIX + ts);
String deviceId = device.getId().getId().toString();
try {
client.handleOneWayDeviceRPCRequestV2(deviceId, PERSISTENT_BODY);
} catch (ApiException e) {
assertEquals("handleOneWayDeviceRPCRequest1 got an unexpected HTTP error: " + e.getCode(),
0, e.getCode());
}
client.deleteDevice(deviceId);
}
@Test
public void testHandleTwoWayDeviceRPCRequest() throws Exception {
long ts = System.currentTimeMillis();
Device device = createNewDevice(TEST_PREFIX + ts);
String deviceId = device.getId().getId().toString();
try {
client.handleTwoWayDeviceRPCRequestV2(deviceId, PERSISTENT_BODY);
} catch (ApiException e) {
assertEquals("handleTwoWayDeviceRPCRequest1 got an unexpected HTTP error: " + e.getCode(),
0, e.getCode());
}
client.deleteDevice(deviceId);
}
@Test
public void testGetPersistedRpcAndDeleteRpc() throws Exception {
long ts = System.currentTimeMillis();
Device device = createNewDevice(TEST_PREFIX + ts);
String deviceId = device.getId().getId().toString();
String rpcId = postPersistentRpcAndGetId(deviceId);
assertNotNull(rpcId);
Rpc rpc = client.getPersistedRpc(rpcId);
assertNotNull(rpc);
assertNotNull(rpc.getId());
client.deleteRpc(rpcId);
assertReturns404(() -> client.getPersistedRpc(rpcId));
client.deleteDevice(deviceId);
}
@Test
public void testGetPersistedRpcNotFound() {
assertReturns404(() -> client.getPersistedRpc(UUID.randomUUID().toString()));
}
@Test
public void testGetPersistedRpcByDevice() throws Exception {
long ts = System.currentTimeMillis();
Device device = createNewDevice(TEST_PREFIX + ts);
String deviceId = device.getId().getId().toString();
postPersistentRpcAndGetId(deviceId);
try {
client.getPersistedRpcByDevice(deviceId, 100, 0, null, null, null, null);
} catch (ApiException e) {
assertEquals("getPersistedRpcByDevice got an unexpected HTTP error: " + e.getCode(),
0, e.getCode());
}
client.deleteDevice(deviceId);
}
private Device createNewDevice(String name) throws ApiException {
Device device = new Device();
device.setName(name);
device.setType("default");
return client.saveDevice(device, null, null, null, null);
}
private String postPersistentRpcAndGetId(String deviceId) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(getBaseUrl() + "/api/plugins/rpc/oneway/" + deviceId))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + client.getToken())
.POST(HttpRequest.BodyPublishers.ofString(PERSISTENT_BODY))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
return OBJECT_MAPPER.readTree(response.body()).get("rpcId").asText();
}
}

163
application/src/test/java/org/thingsboard/server/client/RuleChainApiClientTest.java

@ -0,0 +1,163 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.NodeConnectionInfo;
import org.thingsboard.client.model.PageDataRuleChain;
import org.thingsboard.client.model.RuleChain;
import org.thingsboard.client.model.RuleChainMetaData;
import org.thingsboard.client.model.RuleChainType;
import org.thingsboard.client.model.RuleNode;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class RuleChainApiClientTest extends AbstractApiClientTest {
@Test
public void testRuleChainAndNodeLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<RuleChain> createdChains = new ArrayList<>();
// create 5 rule chains
for (int i = 0; i < 5; i++) {
RuleChain ruleChain = new RuleChain();
ruleChain.setName(TEST_PREFIX + "RuleChain_" + timestamp + "_" + i);
ruleChain.setType(RuleChainType.CORE);
ruleChain.setDebugMode(false);
RuleChain created = client.saveRuleChain(ruleChain);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals(ruleChain.getName(), created.getName());
assertEquals(RuleChainType.CORE, created.getType());
createdChains.add(created);
}
// list rule chains with text search
PageDataRuleChain filteredChains = client.getRuleChains(100, 0, null,
TEST_PREFIX + "RuleChain_" + timestamp, null, null);
assertNotNull(filteredChains);
assertEquals(5, filteredChains.getData().size());
// get rule chain by id
RuleChain searchChain = createdChains.get(2);
RuleChain fetchedChain = client.getRuleChainById(searchChain.getId().getId().toString());
assertEquals(searchChain.getName(), fetchedChain.getName());
assertEquals(searchChain.getType(), fetchedChain.getType());
// get metadata (initially has default node)
RuleChainMetaData metadata = client.getRuleChainMetaData(searchChain.getId().getId().toString());
assertNotNull(metadata);
assertEquals(searchChain.getId().getId(), metadata.getRuleChainId().getId());
// save metadata with rule nodes and connections
RuleChainMetaData newMetadata = new RuleChainMetaData(metadata.getRuleChainId());
newMetadata.setVersion(metadata.getVersion());
newMetadata.setFirstNodeIndex(0);
// node 0: message type switch
RuleNode switchNode = new RuleNode();
switchNode.setName("Message Type Switch");
switchNode.setType("org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode");
switchNode.setConfiguration(OBJECT_MAPPER.createObjectNode().put("version", 0));
switchNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 200).put("layoutY", 150));
// node 1: log node for telemetry
RuleNode logNode = new RuleNode();
logNode.setName("Log Telemetry");
logNode.setType("org.thingsboard.rule.engine.action.TbLogNode");
logNode.setConfiguration(OBJECT_MAPPER.createObjectNode()
.put("scriptLang", "TBEL")
.put("jsScript", "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);")
.put("tbelScript", "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"));
logNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 500).put("layoutY", 100));
// node 2: save timeseries
RuleNode saveNode = new RuleNode();
saveNode.setName("Save Timeseries");
saveNode.setType("org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode");
saveNode.setConfiguration(OBJECT_MAPPER.createObjectNode()
.put("defaultTTL", 0)
.put("skipLatestPersistence", false)
.put("useServerTs", false));
saveNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 500).put("layoutY", 250));
newMetadata.setNodes(List.of(switchNode, logNode, saveNode));
// connection: switch -> log (on "Post telemetry")
NodeConnectionInfo conn1 = new NodeConnectionInfo();
conn1.setFromIndex(0);
conn1.setToIndex(1);
conn1.setType("Post telemetry");
// connection: switch -> save timeseries (on "Post telemetry")
NodeConnectionInfo conn2 = new NodeConnectionInfo();
conn2.setFromIndex(0);
conn2.setToIndex(2);
conn2.setType("Post telemetry");
newMetadata.setConnections(List.of(conn1, conn2));
newMetadata.setRuleChainConnections(List.of());
RuleChainMetaData savedMetadata = client.saveRuleChainMetaData(newMetadata, false);
assertNotNull(savedMetadata);
assertEquals(3, savedMetadata.getNodes().size());
assertEquals(2, savedMetadata.getConnections().size());
// verify saved nodes
RuleChainMetaData fetchedMetadata = client.getRuleChainMetaData(searchChain.getId().getId().toString());
assertEquals(3, fetchedMetadata.getNodes().size());
assertTrue(fetchedMetadata.getNodes().stream()
.anyMatch(node -> "Log Telemetry".equals(node.getName())));
assertTrue(fetchedMetadata.getNodes().stream()
.anyMatch(node -> "Save Timeseries".equals(node.getName())));
// get output labels
client.getRuleChainOutputLabels(searchChain.getId().getId().toString());
// update rule chain
RuleChain chainToUpdate = createdChains.get(3);
chainToUpdate.setName(chainToUpdate.getName() + "_updated");
chainToUpdate.setDebugMode(true);
RuleChain updatedChain = client.saveRuleChain(chainToUpdate);
assertEquals(chainToUpdate.getName(), updatedChain.getName());
assertEquals(true, updatedChain.getDebugMode());
// delete rule chain
UUID chainToDeleteId = createdChains.get(0).getId().getId();
client.deleteRuleChain(chainToDeleteId.toString());
// verify deletion
assertReturns404(() ->
client.getRuleChainById(chainToDeleteId.toString())
);
PageDataRuleChain chainsAfterDelete = client.getRuleChains(100, 0, null,
TEST_PREFIX + "RuleChain_" + timestamp, null, null);
assertEquals(4, chainsAfterDelete.getData().size());
}
}

151
application/src/test/java/org/thingsboard/server/client/TbImageApiClientTest.java

@ -0,0 +1,151 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.PageDataTbResourceInfo;
import org.thingsboard.client.model.ResourceExportData;
import org.thingsboard.client.model.TbImageDeleteResult;
import org.thingsboard.client.model.TbResourceInfo;
import org.thingsboard.server.dao.service.DaoSqlTest;
import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class TbImageApiClientTest extends AbstractApiClientTest {
private File createTempImage(String name, Color color) throws IOException {
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
Graphics2D g = img.createGraphics();
g.setColor(color);
g.fillRect(0, 0, 100, 100);
g.dispose();
File tempFile = File.createTempFile(name, ".png");
tempFile.deleteOnExit();
ImageIO.write(img, "png", tempFile);
return tempFile;
}
@Test
public void testImageLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<TbResourceInfo> createdImages = new ArrayList<>();
Color[] colors = {Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.CYAN};
// upload 5 images
for (int i = 0; i < 5; i++) {
String title = TEST_PREFIX + "Image_" + timestamp + "_" + i;
File imageFile = createTempImage("test_image_" + i, colors[i]);
TbResourceInfo uploaded = client.uploadImage(imageFile, title, null);
assertNotNull(uploaded);
assertNotNull(uploaded.getResourceKey());
assertEquals(title, uploaded.getTitle());
assertNotNull(uploaded.getLink());
createdImages.add(uploaded);
}
// list images with text search
PageDataTbResourceInfo filteredImages = client.getImages(100, 0, null, false,
TEST_PREFIX + "Image_" + timestamp, null, null);
assertNotNull(filteredImages);
assertEquals(5, filteredImages.getData().size());
// get image info by type and key
TbResourceInfo searchImage = createdImages.get(2);
TbResourceInfo fetchedInfo = client.getImageInfo("tenant", searchImage.getResourceKey());
assertEquals(searchImage.getTitle(), fetchedInfo.getTitle());
assertEquals(searchImage.getResourceKey(), fetchedInfo.getResourceKey());
// download image
File downloadedImage = client.downloadImage("tenant", searchImage.getResourceKey(), null, null);
assertNotNull(downloadedImage);
assertTrue(downloadedImage.exists());
assertTrue(downloadedImage.length() > 0);
// download image preview
File preview = client.downloadImagePreview("tenant", searchImage.getResourceKey(), null, null);
assertNotNull(preview);
assertTrue(preview.exists());
assertTrue(preview.length() > 0);
// update image file
File updatedImageFile = createTempImage("updated_image", Color.MAGENTA);
TbResourceInfo updatedImage = client.updateImage("tenant", searchImage.getResourceKey(), updatedImageFile);
assertNotNull(updatedImage);
assertEquals(searchImage.getResourceKey(), updatedImage.getResourceKey());
// update image info (title)
TbResourceInfo infoToUpdate = client.getImageInfo("tenant", createdImages.get(3).getResourceKey());
infoToUpdate.setTitle(infoToUpdate.getTitle() + "_updated");
TbResourceInfo updatedInfo = client.updateImageInfo("tenant", infoToUpdate.getResourceKey(), infoToUpdate);
assertEquals(infoToUpdate.getTitle(), updatedInfo.getTitle());
// make image public
TbResourceInfo publicImage = client.updateImagePublicStatus("tenant",
createdImages.get(1).getResourceKey(), true);
assertTrue(publicImage.getPublic());
assertNotNull(publicImage.getPublicResourceKey());
assertNotNull(publicImage.getPublicLink());
// download public image
File publicDownload = client.downloadPublicImage(publicImage.getPublicResourceKey(), null, null);
assertNotNull(publicDownload);
assertTrue(publicDownload.exists());
assertTrue(publicDownload.length() > 0);
// make image private again
TbResourceInfo privateImage = client.updateImagePublicStatus("tenant",
createdImages.get(1).getResourceKey(), false);
assertEquals(false, privateImage.getPublic());
// export image
ResourceExportData exportData = client.exportImage("tenant", createdImages.get(4).getResourceKey());
assertNotNull(exportData);
assertNotNull(exportData.getData());
assertEquals(createdImages.get(4).getTitle(), exportData.getTitle());
assertEquals(createdImages.get(4).getResourceKey(), exportData.getResourceKey());
// delete image
String keyToDelete = createdImages.get(0).getResourceKey();
TbImageDeleteResult deleteResult = client.deleteImage("tenant", keyToDelete, false);
assertNotNull(deleteResult);
assertTrue(deleteResult.getSuccess());
// verify deletion
assertReturns404(() ->
client.getImageInfo("tenant", keyToDelete)
);
PageDataTbResourceInfo imagesAfterDelete = client.getImages(100, 0, null, false,
TEST_PREFIX + "Image_" + timestamp, null, null);
assertEquals(4, imagesAfterDelete.getData().size());
}
}

128
application/src/test/java/org/thingsboard/server/client/TbResourceApiClientTest.java

@ -0,0 +1,128 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.PageDataTbResourceInfo;
import org.thingsboard.client.model.ResourceType;
import org.thingsboard.client.model.TbResource;
import org.thingsboard.client.model.TbResourceInfo;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.io.File;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class TbResourceApiClientTest extends AbstractApiClientTest {
@Test
public void testResourceLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<TbResourceInfo> createdResources = new ArrayList<>();
// create 5 JS_MODULE resources
for (int i = 0; i < 5; i++) {
TbResource resource = new TbResource();
resource.setTitle(TEST_PREFIX + "Resource_" + timestamp + "_" + i);
resource.setResourceType(ResourceType.JS_MODULE);
resource.setResourceKey("test_module_" + timestamp + "_" + i + ".js");
resource.setFileName("test_module_" + timestamp + "_" + i + ".js");
String jsContent = "export default function test" + i + "() { return " + i + "; }";
resource.setData(Base64.getEncoder().encodeToString(jsContent.getBytes()));
TbResourceInfo created = client.saveResource(resource);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals(resource.getTitle(), created.getTitle());
assertEquals(ResourceType.JS_MODULE, created.getResourceType());
createdResources.add(created);
}
// get tenant resources, check count
PageDataTbResourceInfo tenantResources = client.getTenantResources(100, 0, null, null, null);
assertNotNull(tenantResources);
assertNotNull(tenantResources.getData());
int initialSize = tenantResources.getData().size();
assertTrue("Expected at least 5 resources, but got " + initialSize, initialSize >= 5);
// find with text search
PageDataTbResourceInfo filteredResources = client.getTenantResources(100, 0,
TEST_PREFIX + "Resource_" + timestamp, null, null);
assertEquals(5, filteredResources.getData().size());
// get resources with type filter
PageDataTbResourceInfo jsResources = client.getResources(100, 0,
ResourceType.JS_MODULE.getValue(), null, TEST_PREFIX + "Resource_" + timestamp, null, null);
assertEquals(5, jsResources.getData().size());
// get resource info by id
TbResourceInfo searchResource = createdResources.get(2);
TbResourceInfo fetchedInfo = client.getResourceInfoById(searchResource.getId().getId().toString());
assertEquals(searchResource.getTitle(), fetchedInfo.getTitle());
assertEquals(searchResource.getResourceKey(), fetchedInfo.getResourceKey());
// get full resource by id (includes data)
TbResource fullResource = client.getResourceById(searchResource.getId().getId().toString());
assertNotNull(fullResource);
assertEquals(searchResource.getTitle(), fullResource.getTitle());
assertNotNull(fullResource.getData());
// download resource
File downloadedFile = client.downloadResource(searchResource.getId().getId().toString());
assertNotNull(downloadedFile);
assertTrue(downloadedFile.exists());
assertTrue(downloadedFile.length() > 0);
// get resources by list of ids
List<String> idsToFetch = List.of(
createdResources.get(0).getId().getId().toString(),
createdResources.get(1).getId().getId().toString()
);
List<TbResourceInfo> resourceList = client.getSystemOrTenantResourcesByIds(idsToFetch);
assertEquals(2, resourceList.size());
// update resource
TbResource resourceToUpdate = client.getResourceById(createdResources.get(3).getId().getId().toString());
resourceToUpdate.setTitle(resourceToUpdate.getTitle() + "_updated");
String updatedContent = "export default function updated() { return 42; }";
resourceToUpdate.setData(Base64.getEncoder().encodeToString(updatedContent.getBytes()));
TbResourceInfo updatedResource = client.saveResource(resourceToUpdate);
assertEquals(resourceToUpdate.getTitle(), updatedResource.getTitle());
// delete resource
UUID resourceToDeleteId = createdResources.get(0).getId().getId();
client.deleteResource(resourceToDeleteId.toString(), false);
// verify deletion
assertReturns404(() ->
client.getResourceInfoById(resourceToDeleteId.toString())
);
PageDataTbResourceInfo resourcesAfterDelete = client.getTenantResources(100, 0,
TEST_PREFIX + "Resource_" + timestamp, null, null);
assertEquals(4, resourcesAfterDelete.getData().size());
}
}

150
application/src/test/java/org/thingsboard/server/client/TelemetryApiClientTest.java

@ -0,0 +1,150 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.AttributeData;
import org.thingsboard.client.model.Device;
import org.thingsboard.client.model.TsData;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class TelemetryApiClientTest extends AbstractApiClientTest {
@Test
public void testTelemetryLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
// create a device for telemetry operations
Device device = new Device();
device.setName("TelemetryTestDevice_" + timestamp);
device.setType("default");
Device createdDevice = client.saveDevice(device, null, null, null, null);
assertNotNull(createdDevice);
String entityType = "DEVICE";
String entityId = createdDevice.getId().getId().toString();
// save server-side attributes
String serverAttributes = "{\"serverAttr1\": \"value1\", \"serverAttr2\": 42}";
client.saveEntityAttributesV2(entityType, entityId, "SERVER_SCOPE", serverAttributes);
// save shared attributes
String sharedAttributes = "{\"sharedAttr1\": \"sharedValue1\", \"sharedAttr2\": true}";
client.saveEntityAttributesV2(entityType, entityId, "SHARED_SCOPE", sharedAttributes);
// get attribute keys
List<String> allKeys = client.getAttributeKeys(entityType, entityId);
assertNotNull(allKeys);
assertTrue(allKeys.containsAll(List.of("serverAttr1", "serverAttr2", "sharedAttr1", "sharedAttr2")));
// get attribute keys by scope
List<String> serverKeys = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE");
assertEquals(2 + 1, serverKeys.size()); //active attribute is automatically added to server scope
assertTrue(serverKeys.containsAll(List.of("serverAttr1", "serverAttr2", "active")));
// get attributes by scope
List<AttributeData> serverAttrs = client.getAttributesByScope(entityType, entityId, "SERVER_SCOPE", "serverAttr1,serverAttr2", null);
assertNotNull(serverAttrs);
assertEquals(2, serverAttrs.size());
// get all attributes
List<AttributeData> allAttrs = client.getAttributes(entityType, entityId, "serverAttr1,sharedAttr1", null);
assertEquals(2, allAttrs.size());
assertEquals("value1", allAttrs.stream().filter(attr -> attr.getKey().equals("serverAttr1")).findFirst().orElseThrow().getValue().toString());
assertEquals("sharedValue1", allAttrs.stream().filter(attr -> attr.getKey().equals("sharedAttr1")).findFirst().orElseThrow().getValue().toString());
// save timeseries data
long ts1 = timestamp - 60000;
long ts2 = timestamp - 30000;
long ts3 = timestamp;
String telemetryBody = "{\"ts\":" + ts1 + ",\"values\":{\"temperature\":25.5,\"humidity\":60}}";
client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody);
String telemetryBody2 = "{\"ts\":" + ts2 + ",\"values\":{\"temperature\":26.0,\"humidity\":58}}";
client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody2);
String telemetryBody3 = "{\"ts\":" + ts3 + ",\"values\":{\"temperature\":27.1,\"humidity\":55}}";
client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody3);
// get timeseries keys
List<String> tsKeys = client.getTimeseriesKeys(entityType, entityId);
assertNotNull(tsKeys);
assertEquals(2, tsKeys.size());
assertTrue(tsKeys.containsAll(List.of("humidity", "temperature")));
// get latest timeseries
Map<String, List<TsData>> latestData = client.getLatestTimeseries(entityType, entityId, "temperature,humidity", false, null);
assertNotNull(latestData);
assertNotNull(latestData.get("temperature"));
assertFalse(latestData.get("temperature").isEmpty());
assertEquals("27.1", latestData.get("temperature").get(0).getValue().toString());
// get timeseries history
Map<String, List<TsData>> historyData = client.getTimeseriesHistory(
entityType, entityId,
ts1 - 1000, ts3 + 1000, "temperature",
null, null, null, null, "NONE", "ASC", false, null);
assertNotNull(historyData);
List<TsData> tempHistory = historyData.get("temperature");
assertNotNull(tempHistory);
assertEquals(3, tempHistory.size());
assertEquals("25.5", tempHistory.get(0).getValue().toString());
assertEquals("27.1", tempHistory.get(2).getValue().toString());
// delete timeseries
client.deleteEntityTimeseries(entityType, entityId, "humidity", true, null, null, true, false, null);
List<String> keysAfterDelete = client.getTimeseriesKeys(entityType, entityId);
assertFalse(keysAfterDelete.contains("humidity"));
// delete attributes
client.deleteEntityAttributes(entityType, entityId, "SERVER_SCOPE", "serverAttr1", null);
List<String> serverKeysAfterDelete = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE");
assertFalse(serverKeysAfterDelete.contains("serverAttr1"));
assertTrue(serverKeysAfterDelete.contains("serverAttr2"));
// save device attributes using device-specific endpoint
client.saveDeviceAttributes(entityId, "SERVER_SCOPE", "{\"deviceSpecificAttr\": \"test\"}");
List<String> deviceKeys = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE");
assertTrue(deviceKeys.contains("deviceSpecificAttr"));
// delete device attributes
client.deleteDeviceAttributes(entityId, "SERVER_SCOPE", "deviceSpecificAttr", null);
List<String> deviceKeysAfterDelete = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE");
assertFalse(deviceKeysAfterDelete.contains("deviceSpecificAttr"));
// save telemetry with TTL
String ttlTelemetry = "{\"ts\":" + timestamp + ",\"values\":{\"shortLived\":99}}";
client.saveEntityTelemetryWithTTL(entityType, entityId, "ANY", 86400L, ttlTelemetry);
Map<String, List<TsData>> latestWithTtl = client.getLatestTimeseries(entityType, entityId, "shortLived", false, null);
assertNotNull(latestWithTtl.get("shortLived"));
assertEquals("99", latestWithTtl.get("shortLived").get(0).getValue().toString());
}
}

119
application/src/test/java/org/thingsboard/server/client/TenantApiClientTest.java

@ -0,0 +1,119 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.ApiException;
import org.thingsboard.client.model.Authority;
import org.thingsboard.client.model.PageDataTenant;
import org.thingsboard.client.model.PageDataUser;
import org.thingsboard.client.model.Tenant;
import org.thingsboard.client.model.User;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@DaoSqlTest
public class TenantApiClientTest extends AbstractApiClientTest {
@Test
public void testTenantLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<Tenant> createdTenants = new ArrayList<>();
// authenticate as sysadmin for tenant management
client.login("sysadmin@thingsboard.org", "sysadmin");
// create 20 tenants
for (int i = 0; i < 20; i++) {
Tenant tenant = new Tenant();
String tenantTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i;
tenant.setTitle(tenantTitle);
tenant.setEmail("tenant_" + timestamp + "_" + i + "@test.com");
tenant.setCountry("US");
tenant.setCity("City" + i);
Tenant createdTenant = client.saveTenant(tenant);
assertNotNull(createdTenant);
assertNotNull(createdTenant.getId());
assertEquals(tenantTitle, createdTenant.getTitle());
createdTenants.add(createdTenant);
}
try {
// find all with search text, check count
PageDataTenant filteredTenants = client.getTenants(100, 0, TEST_PREFIX_2, null, null);
assertEquals("Expected exactly 10 tenants matching prefix", 10, filteredTenants.getData().size());
// find by id
Tenant searchTenant = createdTenants.get(10);
Tenant fetchedTenant = client.getTenantById(searchTenant.getId().getId().toString());
assertEquals(searchTenant.getTitle(), fetchedTenant.getTitle());
assertEquals(searchTenant.getEmail(), fetchedTenant.getEmail());
// update tenant
fetchedTenant.setCity("Updated City");
fetchedTenant.setCountry("DE");
Tenant updatedTenant = client.saveTenant(fetchedTenant);
assertEquals("Updated City", updatedTenant.getCity());
assertEquals("DE", updatedTenant.getCountry());
// create a tenant admin for one of the tenants and verify listing
Tenant tenantForAdmin = createdTenants.get(0);
User adminUser = new User();
adminUser.setEmail("tenanttest_admin_" + timestamp + "@test.com");
adminUser.setAuthority(Authority.TENANT_ADMIN);
adminUser.setTenantId(tenantForAdmin.getId());
adminUser.setFirstName("TestAdmin");
User savedAdmin = client.saveUser(adminUser, "false");
assertNotNull(savedAdmin);
PageDataUser tenantAdmins = client.getTenantAdmins(
tenantForAdmin.getId().getId().toString(), 100, 0, null, null, null);
assertEquals(1, tenantAdmins.getData().size());
assertEquals(savedAdmin.getEmail(), tenantAdmins.getData().get(0).getEmail());
// delete tenant
UUID tenantToDeleteId = createdTenants.get(0).getId().getId();
client.deleteTenant(tenantToDeleteId.toString());
createdTenants.remove(0);
// verify deletion
PageDataTenant tenantsAfterDelete = client.getTenants(100, 0, TEST_PREFIX_2, null, null);
assertEquals(10, tenantsAfterDelete.getData().size());
assertReturns404(() ->
client.getTenantById(tenantToDeleteId.toString())
);
} finally {
// clean up all created tenants (deleting tenant cascades to users)
client.login("sysadmin@thingsboard.org", "sysadmin");
for (Tenant tenant : createdTenants) {
try {
client.deleteTenant(tenant.getId().getId().toString());
} catch (ApiException ignored) {
}
}
}
}
}

179
application/src/test/java/org/thingsboard/server/client/TenantProfileApiClientTest.java

@ -0,0 +1,179 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.ApiException;
import org.thingsboard.client.model.DefaultTenantProfileConfiguration;
import org.thingsboard.client.model.EntityInfo;
import org.thingsboard.client.model.PageDataEntityInfo;
import org.thingsboard.client.model.PageDataTenantProfile;
import org.thingsboard.client.model.TenantProfile;
import org.thingsboard.client.model.TenantProfileData;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class TenantProfileApiClientTest extends AbstractApiClientTest {
@Test
public void testTenantProfileLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<TenantProfile> createdProfiles = new ArrayList<>();
// authenticate as sysadmin for tenant profile management
client.login("sysadmin@thingsboard.org", "sysadmin");
// get initial count (there should be a default profile)
PageDataTenantProfile initialProfiles = client.getTenantProfiles(100, 0, null, null, null);
assertNotNull(initialProfiles);
int initialSize = initialProfiles.getData().size();
assertTrue("Expected at least 1 default tenant profile", initialSize >= 1);
// get default tenant profile info
EntityInfo defaultProfileInfo = client.getDefaultTenantProfileInfo();
assertNotNull(defaultProfileInfo);
assertNotNull(defaultProfileInfo.getName());
try {
// create 5 tenant profiles
for (int i = 0; i < 5; i++) {
TenantProfile profile = new TenantProfile();
profile.setName(TEST_PREFIX + "TenantProfile_" + timestamp + "_" + i);
profile.setDescription("Test tenant profile " + i);
profile.setIsolatedTbRuleEngine(false);
TenantProfileData profileData = new TenantProfileData();
DefaultTenantProfileConfiguration config = new DefaultTenantProfileConfiguration();
config.setMaxDevices(100L);
config.setMaxAssets(100L);
config.setMaxCustomers(50L);
config.setMaxUsers(50L);
config.setMaxDashboards(50L);
config.setMaxRuleChains(20L);
config.setMaxDataPointsPerRollingArg(20L);
config.setMaxRelatedEntitiesToReturnPerCfArgument(20);
config.setMaxRelationLevelPerCfArgument(20);
profileData.setConfiguration(config);
profile.setProfileData(profileData);
profile.setDefault(false);
TenantProfile created = client.saveTenantProfile(profile);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals(profile.getName(), created.getName());
assertEquals(profile.getDescription(), created.getDescription());
assertFalse(created.getDefault());
createdProfiles.add(created);
}
// find all, check count
PageDataTenantProfile allProfiles = client.getTenantProfiles(100, 0, null, null, null);
assertNotNull(allProfiles);
assertEquals(initialSize + 5, allProfiles.getData().size());
// find with text search
PageDataTenantProfile filteredProfiles = client.getTenantProfiles(100, 0,
TEST_PREFIX + "TenantProfile_" + timestamp, null, null);
assertEquals(5, filteredProfiles.getData().size());
// get by id
TenantProfile searchProfile = createdProfiles.get(2);
TenantProfile fetchedProfile = client.getTenantProfileById(searchProfile.getId().getId().toString());
assertEquals(searchProfile.getName(), fetchedProfile.getName());
assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription());
// update tenant profile
fetchedProfile.setDescription("Updated description");
TenantProfile updatedProfile = client.saveTenantProfile(fetchedProfile);
assertEquals("Updated description", updatedProfile.getDescription());
assertEquals(fetchedProfile.getName(), updatedProfile.getName());
// get tenant profile infos (paginated)
PageDataEntityInfo profileInfos = client.getTenantProfileInfos(100, 0, null, null, null);
assertNotNull(profileInfos);
assertEquals(initialSize + 5, profileInfos.getData().size());
// get profiles by list of ids
List<String> idsToFetch = List.of(
createdProfiles.get(0).getId().getId().toString(),
createdProfiles.get(1).getId().getId().toString()
);
List<TenantProfile> profileList = client.getTenantProfileList(idsToFetch);
assertEquals(2, profileList.size());
// set a profile as default
TenantProfile profileToSetDefault = createdProfiles.get(1);
client.setDefaultTenantProfile(profileToSetDefault.getId().getId().toString());
EntityInfo defaultTenantProfileInfo = client.getDefaultTenantProfileInfo();
assertEquals(profileToSetDefault.getName(), defaultTenantProfileInfo.getName());
// verify default profile info now points to the new default
EntityInfo newDefaultInfo = client.getDefaultTenantProfileInfo();
assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName());
// restore original default profile
TenantProfile originalDefault = initialProfiles.getData().stream()
.filter(TenantProfile::getDefault)
.findFirst()
.orElseThrow();
client.setDefaultTenantProfile(originalDefault.getId().getId().toString());
// delete tenant profile (cannot delete the default one)
UUID profileToDeleteId = createdProfiles.get(0).getId().getId();
client.deleteTenantProfile(profileToDeleteId.toString());
createdProfiles.remove(0);
// verify deletion
assertReturns404(() ->
client.getTenantProfileById(profileToDeleteId.toString())
);
PageDataTenantProfile profilesAfterDelete = client.getTenantProfiles(100, 0, null, null, null);
assertEquals(initialSize + 4, profilesAfterDelete.getData().size());
} finally {
// clean up created profiles
client.login("sysadmin@thingsboard.org", "sysadmin");
// ensure original default is restored before deleting test profiles
TenantProfile originalDefault = initialProfiles.getData().stream()
.filter(TenantProfile::getDefault)
.findFirst()
.orElseThrow();
try {
client.setDefaultTenantProfile(originalDefault.getId().getId().toString());
} catch (ApiException ignored) {
}
for (TenantProfile profile : createdProfiles) {
try {
client.deleteTenantProfile(profile.getId().getId().toString());
} catch (ApiException ignored) {
}
}
}
}
}

78
application/src/test/java/org/thingsboard/server/client/TwoFactorAuthApiClientTest.java

@ -0,0 +1,78 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.AccountTwoFaSettings;
import org.thingsboard.client.model.PlatformTwoFaSettings;
import org.thingsboard.client.model.TotpTwoFaAccountConfig;
import org.thingsboard.client.model.TotpTwoFaProviderConfig;
import org.thingsboard.client.model.TwoFaAccountConfig;
import org.thingsboard.client.model.TwoFaProviderType;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@DaoSqlTest
public class TwoFactorAuthApiClientTest extends AbstractApiClientTest {
@Test
public void testTwoFactorAuthLifecycle() throws Exception {
// save original platform 2FA settings as sysadmin
client.login("sysadmin@thingsboard.org", "sysadmin");
// configure platform 2FA settings with TOTP provider
TotpTwoFaProviderConfig totpProviderConfig = new TotpTwoFaProviderConfig();
totpProviderConfig.setIssuerName("TestThingsBoard");
PlatformTwoFaSettings newSettings = new PlatformTwoFaSettings();
newSettings.setProviders(List.of(totpProviderConfig));
newSettings.setMinVerificationCodeSendPeriod(30);
newSettings.setTotalAllowedTimeForVerification(300);
newSettings.setMaxVerificationFailuresBeforeUserLockout(5);
PlatformTwoFaSettings savedSettings = client.savePlatformTwoFaSettings(newSettings);
assertNotNull(savedSettings);
assertNotNull(savedSettings.getProviders());
assertFalse(savedSettings.getProviders().isEmpty());
assertEquals(30, savedSettings.getMinVerificationCodeSendPeriod().intValue());
assertEquals(300, savedSettings.getTotalAllowedTimeForVerification().intValue());
// get available 2FA providers (should include TOTP)
List<TwoFaProviderType> providerTypes = client.getAvailableTwoFaProviderTypes();
assertNotNull(providerTypes);
assertTrue(providerTypes.contains(TwoFaProviderType.TOTP));
// get account 2FA settings (should be empty initially)
AccountTwoFaSettings accountSettings = client.getAccountTwoFaSettings();
assertNull(accountSettings);
// generate TOTP account config
TwoFaAccountConfig generatedConfig = client.generateTwoFaAccountConfig(TwoFaProviderType.TOTP.getValue());
assertNotNull(generatedConfig);
TotpTwoFaAccountConfig totpConfig = (TotpTwoFaAccountConfig) generatedConfig;
assertNotNull(totpConfig);
assertNotNull(totpConfig.getAuthUrl());
assertTrue(totpConfig.getAuthUrl().startsWith("otpauth://totp/"));
}
}

135
application/src/test/java/org/thingsboard/server/client/UserApiClientTest.java

@ -0,0 +1,135 @@
/**
* 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.client;
import org.junit.Test;
import org.thingsboard.client.model.Authority;
import org.thingsboard.client.model.Customer;
import org.thingsboard.client.model.JwtPair;
import org.thingsboard.client.model.PageDataUser;
import org.thingsboard.client.model.User;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@DaoSqlTest
public class UserApiClientTest extends AbstractApiClientTest {
@Test
public void testUserLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<User> createdUsers = new ArrayList<>();
// create 20 tenant admin users
for (int i = 0; i < 20; i++) {
User user = new User();
String email = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i + "@test.com";
user.setEmail(email);
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(savedClientTenant.getId());
user.setFirstName("First" + i);
user.setLastName("Last" + i);
User createdUser = client.saveUser(user, "false");
assertNotNull(createdUser);
assertNotNull(createdUser.getId());
assertEquals(email, createdUser.getEmail());
assertEquals(Authority.TENANT_ADMIN, createdUser.getAuthority());
createdUsers.add(createdUser);
}
// find all tenant admins, check count (20 created + 1 from setup)
PageDataUser allUsers = client.getUsers(100, 0, null, null, null);
assertNotNull(allUsers);
assertNotNull(allUsers.getData());
int initialSize = allUsers.getData().size();
assertEquals("Expected 21 users (20 created + 2 from setup), but got " + initialSize, 22, initialSize);
// find with search text, check count
PageDataUser filteredUsers = client.getUsers(100, 0, TEST_PREFIX_2, null, null);
assertEquals("Expected exactly 10 users matching prefix", 10, filteredUsers.getData().size());
// find by id
User searchUser = createdUsers.get(10);
User fetchedUser = client.getUserById(searchUser.getId().getId().toString());
assertEquals(searchUser.getEmail(), fetchedUser.getEmail());
assertEquals(searchUser.getFirstName(), fetchedUser.getFirstName());
// update user
fetchedUser.setFirstName("UpdatedFirst");
fetchedUser.setLastName("UpdatedLast");
User updatedUser = client.saveUser(fetchedUser, "false");
assertEquals("UpdatedFirst", updatedUser.getFirstName());
assertEquals("UpdatedLast", updatedUser.getLastName());
// activate user and get token
activateUser(createdUsers.get(0).getId(), "password123", false);
JwtPair userToken = client.getUserToken(createdUsers.get(0).getId().getId().toString());
assertNotNull(userToken);
assertNotNull(userToken.getToken());
// disable user credentials
client.setUserCredentialsEnabled(createdUsers.get(0).getId().getId().toString(), "false");
// re-enable user credentials
client.setUserCredentialsEnabled(createdUsers.get(0).getId().getId().toString(), "true");
// create customer users and verify listing
Customer customer2 = new Customer();
customer2.setTitle("User test customer " + timestamp);
customer2.setEmail("usertest_" + timestamp + "@test.com");
Customer savedCustomer2 = client.saveCustomer(customer2, null, null, null);
List<User> customerUsers = new ArrayList<>();
for (int i = 0; i < 5; i++) {
User customerUser = new User();
customerUser.setEmail("custuser_" + timestamp + "_" + i + "@test.com");
customerUser.setAuthority(Authority.CUSTOMER_USER);
customerUser.setTenantId(savedClientTenant.getId());
customerUser.setCustomerId(savedCustomer2.getId());
customerUser.setFirstName("CustFirst" + i);
customerUser.setLastName("CustLast" + i);
User created = client.saveUser(customerUser, "false");
assertNotNull(created);
customerUsers.add(created);
}
// list customer users
PageDataUser customerUserPage = client.getCustomerUsers(
savedCustomer2.getId().getId().toString(), 100, 0, null, null, null);
assertEquals("Expected 5 customer users", 5, customerUserPage.getData().size());
// delete user
UUID userToDeleteId = createdUsers.get(0).getId().getId();
client.deleteUser(userToDeleteId.toString());
// verify deletion
PageDataUser usersAfterDelete = client.getUsers(100, 0, null, null, null);
assertEquals(initialSize + 5 - 1, usersAfterDelete.getData().size());
assertReturns404(() ->
client.getUserById(userToDeleteId.toString())
);
}
}

155
application/src/test/java/org/thingsboard/server/client/WidgetTypeApiClientTest.java

@ -0,0 +1,155 @@
/**
* 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.client;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.Test;
import org.thingsboard.client.model.PageDataWidgetTypeInfo;
import org.thingsboard.client.model.WidgetTypeDetails;
import org.thingsboard.client.model.WidgetTypeInfo;
import org.thingsboard.client.model.WidgetsBundle;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@DaoSqlTest
public class WidgetTypeApiClientTest extends AbstractApiClientTest {
private JsonNode createDescriptor(String type) {
return OBJECT_MAPPER.createObjectNode()
.put("type", type)
.put("sizeX", 7.5)
.put("sizeY", 5)
.put("resources", "[]")
.put("templateHtml", "<div class='test-widget'>Test</div>")
.put("templateCss", ".test-widget { font-size: 14px; }")
.put("controllerScript", "self.onInit = function() {};")
.put("settingsSchema", "{}")
.put("dataKeySettingsSchema", "{}");
}
@Test
public void testWidgetTypeLifecycle() throws Exception {
long timestamp = System.currentTimeMillis();
List<WidgetTypeDetails> createdWidgetTypes = new ArrayList<>();
// create a widgets bundle
WidgetsBundle bundle = new WidgetsBundle(null, null, null,
TEST_PREFIX + "Bundle_" + timestamp, null, false,
"Test bundle description", null, null);
WidgetsBundle savedBundle = client.saveWidgetsBundle(bundle);
assertNotNull(savedBundle);
assertNotNull(savedBundle.getId());
assertEquals(bundle.getTitle(), savedBundle.getTitle());
// create 5 widget types
for (int i = 0; i < 5; i++) {
String name = TEST_PREFIX + "Widget_" + timestamp + "_" + i;
JsonNode descriptor = createDescriptor("latest");
WidgetTypeDetails widgetType = new WidgetTypeDetails(null, null, null, name, descriptor);
widgetType.setDescription("Test widget " + i);
widgetType.setDeprecated(false);
widgetType.setTags(List.of("test", "automated"));
WidgetTypeDetails created = client.saveWidgetType(widgetType, false);
assertNotNull(created);
assertNotNull(created.getId());
assertEquals(name, created.getName());
assertNotNull(created.getFqn());
createdWidgetTypes.add(created);
}
// list widget types with text search (tenant only)
PageDataWidgetTypeInfo filteredTypes = client.getWidgetTypes(100, 0,
TEST_PREFIX + "Widget_" + timestamp, null, null,
true, false, null, null, null);
assertNotNull(filteredTypes);
assertEquals(5, filteredTypes.getData().size());
// get widget type details by id
WidgetTypeDetails searchWidget = createdWidgetTypes.get(2);
WidgetTypeDetails fetchedDetails = client.getWidgetTypeById(
searchWidget.getId().getId().toString(), true);
assertEquals(searchWidget.getName(), fetchedDetails.getName());
assertEquals(searchWidget.getFqn(), fetchedDetails.getFqn());
assertEquals("Test widget 2", fetchedDetails.getDescription());
// get widget type info by id
WidgetTypeInfo fetchedInfo = client.getWidgetTypeInfoById(
searchWidget.getId().getId().toString());
assertEquals(searchWidget.getName(), fetchedInfo.getName());
// add widget types to bundle
List<String> widgetTypeIds = createdWidgetTypes.stream()
.map(wt -> wt.getId().getId().toString())
.collect(Collectors.toList());
client.updateWidgetsBundleWidgetTypes(savedBundle.getId().getId().toString(), widgetTypeIds);
// get bundle widget type fqns
List<String> bundleFqns = client.getBundleWidgetTypeFqns(savedBundle.getId().getId().toString());
assertEquals(5, bundleFqns.size());
// get bundle widget types details
List<WidgetTypeDetails> bundleDetails = client.getBundleWidgetTypesDetails(
savedBundle.getId().getId().toString(), false);
assertEquals(5, bundleDetails.size());
// get bundle widget types infos (paginated)
PageDataWidgetTypeInfo bundleInfos = client.getBundleWidgetTypesInfos(
savedBundle.getId().getId().toString(), 100, 0,
null, null, null, null, null, null);
assertEquals(5, bundleInfos.getData().size());
// update widget type
WidgetTypeDetails widgetToUpdate = client.getWidgetTypeById(
createdWidgetTypes.get(3).getId().getId().toString(), true);
widgetToUpdate.setDescription("Updated description");
widgetToUpdate.setDeprecated(true);
widgetToUpdate.setTags(List.of("test", "updated"));
WidgetTypeDetails updatedWidget = client.saveWidgetType(widgetToUpdate, false);
assertEquals("Updated description", updatedWidget.getDescription());
assertEquals(true, updatedWidget.getDeprecated());
// delete widget type
String widgetToDeleteId = createdWidgetTypes.get(0).getId().getId().toString();
client.deleteWidgetType(widgetToDeleteId);
// verify deletion
assertReturns404(() ->
client.getWidgetTypeById(widgetToDeleteId, false)
);
PageDataWidgetTypeInfo typesAfterDelete = client.getWidgetTypes(100, 0,
TEST_PREFIX + "Widget_" + timestamp, null, null,
true, false, null, null, null);
assertEquals(4, typesAfterDelete.getData().size());
// delete widgets bundle
client.deleteWidgetsBundle(savedBundle.getId().getId().toString());
assertReturns404(() ->
client.getWidgetsBundleById(savedBundle.getId().getId().toString(), false)
);
}
}

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

@ -94,6 +94,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.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldInfo;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@ -1482,6 +1483,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return doPost("/api/calculatedField", calculatedField, CalculatedField.class);
}
protected AlarmRuleDefinition saveAlarmRule(AlarmRuleDefinition alarmRule) {
return doPost("/api/alarm/rule", alarmRule, AlarmRuleDefinition.class);
}
protected PageData<CalculatedField> getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception {
return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" +
(type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink);

420
application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java

@ -0,0 +1,420 @@
/**
* 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.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.rule.AlarmRule;
import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class AlarmRuleControllerTest extends AbstractControllerTest {
private Tenant savedTenant;
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
savedTenant = saveTenant(tenant);
assertThat(savedTenant).isNotNull();
User tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
createUserAndLogin(tenantAdmin, "testPassword1");
}
@After
public void afterTest() throws Exception {
loginSysAdmin();
deleteTenant(savedTenant.getId());
}
@Test
public void testSaveAlarmRule() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "High Temperature");
AlarmRuleDefinition saved = saveAlarmRule(alarmRule);
assertThat(saved).isNotNull();
assertThat(saved.getId()).isNotNull();
assertThat(saved.getCreatedTime()).isGreaterThan(0);
assertThat(saved.getTenantId()).isEqualTo(savedTenant.getId());
assertThat(saved.getEntityId()).isEqualTo(testDevice.getId());
assertThat(saved.getName()).isEqualTo("High Temperature");
assertThat(saved.getConfiguration()).isNotNull();
assertThat(saved.getConfiguration().getCreateRules()).containsKey(AlarmSeverity.CRITICAL);
saved.setName("Updated Alarm Rule");
AlarmRuleDefinition updated = saveAlarmRule(saved);
assertThat(updated.getName()).isEqualTo("Updated Alarm Rule");
assertThat(updated.getVersion()).isEqualTo(saved.getVersion() + 1);
doDelete("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetAlarmRuleById() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "Test Alarm");
AlarmRuleDefinition saved = saveAlarmRule(alarmRule);
AlarmRuleDefinition fetched = doGet("/api/alarm/rule/" + saved.getId().getId(), AlarmRuleDefinition.class);
assertThat(fetched).isNotNull();
assertThat(fetched).isEqualTo(saved);
doDelete("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetAlarmRuleById_notFound() throws Exception {
doGet("/api/alarm/rule/" + UUID.randomUUID())
.andExpect(status().isNotFound());
}
@Test
public void testGetAlarmRuleById_calculatedFieldNotAlarm() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField cf = createSimpleCalculatedField(testDevice.getId());
CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class);
doGet("/api/alarm/rule/" + savedCf.getId().getId())
.andExpect(status().isNotFound());
doDelete("/api/calculatedField/" + savedCf.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetAlarmRulesByEntityId() throws Exception {
Device device1 = createDevice("Device 1", "1234567890");
Device device2 = createDevice("Device 2", "0987654321");
AlarmRuleDefinition rule1 = saveAlarmRule(createTestAlarmRule(device1.getId(), "Rule 1"));
saveAlarmRule(createTestAlarmRule(device2.getId(), "Rule 2"));
PageData<AlarmRuleDefinition> result = doGetTypedWithPageLink(
"/api/alarm/rules/" + EntityType.DEVICE + "/" + device1.getUuidId() + "?",
new TypeReference<>() {}, new PageLink(10));
assertThat(result.getData()).hasSize(1);
assertThat(result.getData().get(0).getId()).isEqualTo(rule1.getId());
assertThat(result.getData().get(0).getName()).isEqualTo("Rule 1");
}
@Test
public void testGetAlarmRules() throws Exception {
Device device = createDevice("Device A", "1234567890");
AlarmRuleDefinition deviceRule = saveAlarmRule(createTestAlarmRule(device.getId(), "Device Alarm"));
DeviceProfile profile = doPost("/api/deviceProfile", createDeviceProfile("Profile A"), DeviceProfile.class);
AlarmRuleDefinition profileRule = saveAlarmRule(createTestAlarmRule(profile.getId(), "Profile Alarm"));
// All alarm rules
List<AlarmRuleDefinitionInfo> all = getAlarmRules(null, null);
assertThat(all).extracting(AlarmRuleDefinition::getName)
.contains("Device Alarm", "Profile Alarm");
// Filter by entity type: DEVICE
List<AlarmRuleDefinitionInfo> deviceRules = getAlarmRules(EntityType.DEVICE, null);
assertThat(deviceRules).extracting(AlarmRuleDefinition::getName)
.containsOnly("Device Alarm");
// Filter by entity type: DEVICE_PROFILE
List<AlarmRuleDefinitionInfo> profileRules = getAlarmRules(EntityType.DEVICE_PROFILE, null);
assertThat(profileRules).extracting(AlarmRuleDefinition::getName)
.containsOnly("Profile Alarm");
// Filter by specific entity IDs
List<AlarmRuleDefinitionInfo> specificRules = getAlarmRules(EntityType.DEVICE, List.of(device.getUuidId()));
assertThat(specificRules).extracting(AlarmRuleDefinition::getName)
.containsOnly("Device Alarm");
// Verify entity names are populated
AlarmRuleDefinitionInfo deviceInfo = all.stream()
.filter(r -> r.getName().equals("Device Alarm")).findFirst().orElseThrow();
assertThat(deviceInfo.getEntityName()).isEqualTo("Device A");
AlarmRuleDefinitionInfo profileInfo = all.stream()
.filter(r -> r.getName().equals("Profile Alarm")).findFirst().orElseThrow();
assertThat(profileInfo.getEntityName()).isEqualTo("Profile A");
}
@Test
public void testGetAlarmRules_textSearch() throws Exception {
Device device = createDevice("Device A", "1234567890");
saveAlarmRule(createTestAlarmRule(device.getId(), "Temperature Alarm"));
saveAlarmRule(createTestAlarmRule(device.getId(), "Humidity Alarm"));
PageData<AlarmRuleDefinitionInfo> result = doGetTypedWithPageLink(
"/api/alarm/rules?textSearch=Temp&",
new TypeReference<>() {}, new PageLink(10));
assertThat(result.getData()).hasSize(1);
assertThat(result.getData().get(0).getName()).isEqualTo("Temperature Alarm");
}
@Test
public void testGetAlarmRuleNames() throws Exception {
Device device = createDevice("Device A", "1234567890");
saveAlarmRule(createTestAlarmRule(device.getId(), "Alpha Alarm"));
saveAlarmRule(createTestAlarmRule(device.getId(), "Beta Alarm"));
PageData<String> names = getAlarmRuleNames(new PageLink(10, 0,
null, new SortOrder("", SortOrder.Direction.ASC)));
assertThat(names.getTotalElements()).isEqualTo(2);
assertThat(names.getData()).isSortedAccordingTo(Comparator.naturalOrder());
assertThat(names.getData()).contains("Alpha Alarm", "Beta Alarm");
names = getAlarmRuleNames(new PageLink(10, 0,
null, new SortOrder("", SortOrder.Direction.DESC)));
assertThat(names.getData()).isSortedAccordingTo(Comparator.reverseOrder());
names = getAlarmRuleNames(new PageLink(10, 0,
"Alpha", new SortOrder("", SortOrder.Direction.ASC)));
assertThat(names.getTotalElements()).isEqualTo(1);
assertThat(names.getData()).containsOnly("Alpha Alarm");
}
@Test
public void testDeleteAlarmRule() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "To Delete"));
assertThat(saved).isNotNull();
doDelete("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isOk());
doGet("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isNotFound());
}
@Test
public void testDeleteAlarmRule_notFound() throws Exception {
doDelete("/api/alarm/rule/" + UUID.randomUUID())
.andExpect(status().isNotFound());
}
@Test
public void testDeleteAlarmRule_calculatedFieldNotAlarm() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField cf = createSimpleCalculatedField(testDevice.getId());
CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class);
doDelete("/api/alarm/rule/" + savedCf.getId().getId())
.andExpect(status().isNotFound());
doDelete("/api/calculatedField/" + savedCf.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetLatestAlarmRuleDebugEvent() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "Debug Test"));
doGet("/api/alarm/rule/" + saved.getId().getId() + "/debug")
.andExpect(status().isOk());
doDelete("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetLatestAlarmRuleDebugEvent_notFound() throws Exception {
doGet("/api/alarm/rule/" + UUID.randomUUID() + "/debug")
.andExpect(status().isNotFound());
}
@Test
public void testTestAlarmRuleScript() throws Exception {
JsonNode request = JacksonUtil.toJsonNode("""
{
"expression": "return temperature > 50;",
"arguments": {
"temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 55 }
}
}
""");
JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class);
assertThat(result).isNotNull();
assertThat(result.has("output")).isTrue();
assertThat(result.has("error")).isTrue();
assertThat(result.get("error").asText()).isEmpty();
assertThat(result.get("output").asText()).isEqualTo("true");
}
@Test
public void testTestAlarmRuleScript_returnsFalse() throws Exception {
JsonNode request = JacksonUtil.toJsonNode("""
{
"expression": "return temperature > 50;",
"arguments": {
"temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 30 }
}
}
""");
JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class);
assertThat(result).isNotNull();
assertThat(result.get("error").asText()).isEmpty();
assertThat(result.get("output").asText()).isEqualTo("false");
}
@Test
public void testTestAlarmRuleScript_missingExpression() throws Exception {
JsonNode request = JacksonUtil.toJsonNode("""
{
"arguments": {}
}
""");
doPost("/api/alarm/rule/testScript", request)
.andExpect(status().isBadRequest());
}
@Test
public void testTestAlarmRuleScript_invalidExpression() throws Exception {
JsonNode request = JacksonUtil.toJsonNode("""
{
"expression": "invalid syntax {{{{",
"arguments": {}
}
""");
JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class);
assertThat(result).isNotNull();
assertThat(result.get("error").asText()).isNotEmpty();
}
// --- Helper methods ---
private AlarmRuleDefinition createTestAlarmRule(EntityId entityId, String name) {
AlarmRuleDefinition alarmRule = new AlarmRuleDefinition();
alarmRule.setEntityId(entityId);
alarmRule.setName(name);
alarmRule.setConfigurationVersion(1);
alarmRule.setAdditionalInfo(JacksonUtil.newObjectNode());
AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration();
Argument argument = new Argument();
argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
argument.setDefaultValue("0");
configuration.setArguments(Map.of("temperature", argument));
AlarmRule rule = new AlarmRule();
TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression();
expression.setExpression("return temperature >= 50;");
SimpleAlarmCondition condition = new SimpleAlarmCondition();
condition.setExpression(expression);
rule.setCondition(condition);
configuration.setCreateRules(Map.of(AlarmSeverity.CRITICAL, rule));
alarmRule.setConfiguration(configuration);
return alarmRule;
}
private CalculatedField createSimpleCalculatedField(EntityId entityId) {
CalculatedField cf = new CalculatedField();
cf.setEntityId(entityId);
cf.setType(CalculatedFieldType.SIMPLE);
cf.setName("Simple CF");
cf.setConfigurationVersion(1);
cf.setAdditionalInfo(JacksonUtil.newObjectNode());
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument arg = new Argument();
arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
config.setArguments(Map.of("T", arg));
config.setExpression("T * 2");
TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("result");
config.setOutput(output);
cf.setConfiguration(config);
return cf;
}
private List<AlarmRuleDefinitionInfo> getAlarmRules(EntityType entityType, List<UUID> entities) throws Exception {
StringBuilder url = new StringBuilder("/api/alarm/rules?");
if (entityType != null) {
url.append("entityType=").append(entityType).append("&");
}
if (entities != null) {
url.append("entities=").append(String.join(",",
entities.stream().map(UUID::toString).toList())).append("&");
}
return doGetTypedWithPageLink(url.toString(),
new TypeReference<PageData<AlarmRuleDefinitionInfo>>() {}, new PageLink(10)).getData();
}
private PageData<String> getAlarmRuleNames(PageLink pageLink) throws Exception {
return doGetTypedWithPageLink("/api/alarm/rules/names?",
new TypeReference<PageData<String>>() {}, pageLink);
}
}

1
application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java

@ -266,7 +266,6 @@ public class NotificationEdgeTest extends AbstractEdgeTest {
notificationRule.setTriggerConfig(triggerConfig);
EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM);
Map<Integer, List<UUID>> escalationTable = new HashMap<>();
escalationTable.put(Integer.valueOf("1"), new ArrayList<>());
recipientsConfig.setEscalationTable(escalationTable);

2
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java

@ -234,6 +234,8 @@ public class RelatedEntitiesAggregationCalculatedFieldStateTest {
config.setUseLatestTs(true);
config.setScheduledUpdateInterval(10);
calculatedField.setConfiguration(config);
calculatedField.setVersion(1L);
return calculatedField;

3
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -188,8 +188,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
rule.setTriggerType(triggerConfig.getTriggerType());
rule.setTriggerConfig(triggerConfig);
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(triggerConfig.getTriggerType());
DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(triggerConfig.getTriggerType());
recipientsConfig.setTargets(DaoUtil.toUUIDs(targets));
rule.setRecipientsConfig(recipientsConfig);

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

Loading…
Cancel
Save