apiKeyUpdateMsgOpt = edgeImitator.findMessageByType(ApiKeyUpdateMsg.class);
+ Assert.assertTrue(apiKeyUpdateMsgOpt.isPresent());
+ ApiKeyUpdateMsg apiKeyUpdateMsg = apiKeyUpdateMsgOpt.get();
+ Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType());
+ }
+
+ private ApiKeyInfo createSimpleApiKeyInfo(String description) {
+ ApiKeyInfo apiKeyInfo = new ApiKeyInfo();
+ apiKeyInfo.setTenantId(tenantId);
+ apiKeyInfo.setUserId(tenantAdminUserId);
+ apiKeyInfo.setDescription(description);
+ apiKeyInfo.setEnabled(true);
+ return apiKeyInfo;
+ }
+
+ private ApiKey createSimpleApiKey(String description) {
+ ApiKey apiKey = new ApiKey();
+ apiKey.setTenantId(tenantId);
+ apiKey.setUserId(tenantAdminUserId);
+ apiKey.setDescription(description);
+ apiKey.setEnabled(true);
+ apiKey.setValue("test-api-key-value-" + UUID.randomUUID());
+ return apiKey;
+ }
+
+ private UplinkMsg getDeleteUplinkMsg(UUID uuid) throws InvalidProtocolBufferException {
+ UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder();
+ ApiKeyUpdateMsg.Builder apiKeyDeleteMsgBuilder = ApiKeyUpdateMsg.newBuilder();
+ apiKeyDeleteMsgBuilder.setMsgType(ENTITY_DELETED_RPC_MESSAGE);
+ apiKeyDeleteMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
+ apiKeyDeleteMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
+ testAutoGeneratedCodeByProtobuf(apiKeyDeleteMsgBuilder);
+
+ upLinkMsgBuilder.addApiKeyUpdateMsg(apiKeyDeleteMsgBuilder.build());
+ testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder);
+
+ return upLinkMsgBuilder.build();
+ }
+
+ private UplinkMsg getUplinkMsg(UUID uuid, ApiKey apiKey, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException {
+ UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
+ ApiKeyUpdateMsg.Builder apiKeyUpdateMsgBuilder = ApiKeyUpdateMsg.newBuilder();
+ apiKeyUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
+ apiKeyUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
+ apiKeyUpdateMsgBuilder.setEntity(JacksonUtil.toString(apiKey));
+ apiKeyUpdateMsgBuilder.setMsgType(updateMsgType);
+ testAutoGeneratedCodeByProtobuf(apiKeyUpdateMsgBuilder);
+ uplinkMsgBuilder.addApiKeyUpdateMsg(apiKeyUpdateMsgBuilder.build());
+
+ testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
+
+ return uplinkMsgBuilder.build();
+ }
+
+ private void checkApiKeyOnCloud(UplinkMsg uplinkMsg, UUID uuid, String description) throws Exception {
+ edgeImitator.expectResponsesAmount(1);
+ edgeImitator.sendUplinkMsg(uplinkMsg);
+
+ Assert.assertTrue(edgeImitator.waitForResponses());
+
+ UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
+ Assert.assertTrue(latestResponseMsg.getSuccess());
+
+ ApiKey apiKey = apiKeyService.findApiKeyById(tenantId, new ApiKeyId(uuid));
+ Assert.assertNotNull(apiKey);
+ Assert.assertEquals(description, apiKey.getDescription());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java
index 60701cee1a..52a30f0e85 100644
--- a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java
@@ -73,6 +73,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName());
Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId());
Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration());
+ Assert.assertEquals(calculatedField.getAdditionalInfo(), calculatedFieldFromMsg.getAdditionalInfo());
edgeImitator.expectMessageAmount(1);
savedCalculatedField.setName(UPDATED_CF_NAME);
@@ -229,6 +230,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
config.setOutput(output);
calculatedField.setConfiguration(config);
+ calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
return calculatedField;
}
@@ -260,6 +262,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class);
Assert.assertNotNull(calculatedField);
Assert.assertEquals(resourceTitle, calculatedField.getName());
+ Assert.assertEquals(JacksonUtil.newObjectNode(), calculatedField.getAdditionalInfo());
}
}
diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
index 8cf2b309f6..8208dc4fc9 100644
--- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
+++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
@@ -30,6 +30,7 @@ import org.thingsboard.edge.rpc.EdgeRpcClient;
import org.thingsboard.server.controller.AbstractWebTest;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
+import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@@ -375,6 +376,11 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(aiModelUpdateMsg));
}
}
+ if (downlinkMsg.getApiKeyUpdateMsgCount() > 0) {
+ for (ApiKeyUpdateMsg apiKeyUpdateMsg : downlinkMsg.getApiKeyUpdateMsgList()) {
+ result.add(saveDownlinkMsg(apiKeyUpdateMsg));
+ }
+ }
if (downlinkMsg.hasEdgeConfiguration()) {
result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration()));
}
diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java
new file mode 100644
index 0000000000..04a1b87833
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java
@@ -0,0 +1,272 @@
+/**
+ * Copyright © 2016-2026 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.edge.rpc;
+
+import io.grpc.ManagedChannel;
+import io.grpc.Server;
+import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
+import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.util.io.pem.PemObject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.thingsboard.server.controller.AbstractWebTest;
+import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc;
+
+import java.io.ByteArrayInputStream;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.Security;
+import java.security.cert.X509Certificate;
+import java.security.spec.ECGenParameterSpec;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * Tests for Edge gRPC SSL setup using the production {@link EdgeGrpcService#setupSsl} method.
+ *
+ * Covers:
+ * 1. Separate cert and key PEM inputs
+ * 2. Combined PEM (cert + key in one file)
+ * 3. Encrypted private key with password
+ * 4. Missing key in combined PEM → error
+ *
+ * Each scenario is parameterized across key types: RSA-2048, RSA-4096, EC P-256, EC P-384.
+ */
+class EdgeGrpcSslTest {
+
+ static {
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ }
+
+ enum KeyType {
+ RSA_2048("RSA", 2048, null, "SHA256withRSA"),
+ RSA_4096("RSA", 4096, null, "SHA256withRSA"),
+ EC_P256("EC", 256, "secp256r1", "SHA256withECDSA"),
+ EC_P384("EC", 384, "secp384r1", "SHA384withECDSA");
+
+ final String algorithm;
+ final int size;
+ final String curve;
+ final String sigAlg;
+
+ KeyType(String algorithm, int size, String curve, String sigAlg) {
+ this.algorithm = algorithm;
+ this.size = size;
+ this.curve = curve;
+ this.sigAlg = sigAlg;
+ }
+
+ KeyPair generateKeyPair() throws Exception {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance(algorithm);
+ if (curve != null) {
+ kpg.initialize(new ECGenParameterSpec(curve));
+ } else {
+ kpg.initialize(size);
+ }
+ return kpg.generateKeyPair();
+ }
+ }
+
+ private final List tempFiles = new ArrayList<>();
+ private Server server;
+ private ManagedChannel channel;
+
+ @AfterEach
+ void cleanup() throws Exception {
+ if (channel != null) {
+ channel.shutdownNow().awaitTermination(2, TimeUnit.SECONDS);
+ }
+ if (server != null) {
+ server.shutdownNow().awaitTermination(2, TimeUnit.SECONDS);
+ }
+ for (Path p : tempFiles) {
+ Files.deleteIfExists(p);
+ }
+ }
+
+ @ParameterizedTest(name = "separateCertAndKey_{0}")
+ @EnumSource(KeyType.class)
+ void separateCertAndKey(KeyType keyType) throws Exception {
+ KeyPair kp = keyType.generateKeyPair();
+ X509Certificate cert = generateSelfSignedCert(kp, keyType.sigAlg);
+
+ Path certFile = writeTempPem("cert", cert);
+ Path keyFile = writeTempPem("key", kp.getPrivate());
+
+ server = startServer(certFile.toString(), keyFile.toString(), null);
+ assertTlsConnectivity(cert);
+ }
+
+ @ParameterizedTest(name = "combinedPemWithCertAndKey_{0}")
+ @EnumSource(KeyType.class)
+ void combinedPemWithCertAndKey(KeyType keyType) throws Exception {
+ KeyPair kp = keyType.generateKeyPair();
+ X509Certificate cert = generateSelfSignedCert(kp, keyType.sigAlg);
+
+ Path combinedFile = writeTempPem("combined", cert, kp.getPrivate());
+
+ server = startServer(combinedFile.toString(), "", null);
+ assertTlsConnectivity(cert);
+ }
+
+ // RSA-only: BouncyCastle writes encrypted EC keys in traditional PEM format (BEGIN EC PRIVATE KEY),
+ // which after decryption produces a PEMKeyPair without public key info — causing PemSslCredentials
+ // to fail with "Cannot invoke SubjectPublicKeyInfo.getEncoded() because getPublicKeyInfo() is null".
+ @ParameterizedTest(name = "encryptedPrivateKey_{0}")
+ @EnumSource(value = KeyType.class, names = {"RSA_2048", "RSA_4096"})
+ void encryptedPrivateKey(KeyType keyType) throws Exception {
+ KeyPair kp = keyType.generateKeyPair();
+ X509Certificate cert = generateSelfSignedCert(kp, keyType.sigAlg);
+ String password = "test-password";
+
+ Path combinedFile = writeTempPemEncrypted("enc-combined", password, cert, kp.getPrivate());
+
+ server = startServer(combinedFile.toString(), "", password);
+ assertTlsConnectivity(cert);
+ }
+
+ @ParameterizedTest(name = "combinedPemWithCertOnly_throwsException_{0}")
+ @EnumSource(KeyType.class)
+ void combinedPemWithCertOnly_throwsException(KeyType keyType) throws Exception {
+ KeyPair kp = keyType.generateKeyPair();
+ X509Certificate cert = generateSelfSignedCert(kp, keyType.sigAlg);
+
+ Path certOnlyFile = writeTempPem("cert-only", cert);
+
+ assertThatThrownBy(() -> startServer(certOnlyFile.toString(), "", null))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ // --- Server startup using production EdgeGrpcService.setupSsl() ---
+
+ private Server startServer(String certFileResource, String privateKeyResource, String keyPassword) throws Exception {
+ EdgeGrpcService edgeGrpcService = new EdgeGrpcService();
+ ReflectionTestUtils.setField(edgeGrpcService, "certFileResource", certFileResource);
+ ReflectionTestUtils.setField(edgeGrpcService, "privateKeyResource", privateKeyResource);
+ ReflectionTestUtils.setField(edgeGrpcService, "keyPassword", keyPassword != null ? keyPassword : "");
+
+ NettyServerBuilder builder = NettyServerBuilder.forPort(0)
+ .addService(new EdgeRpcServiceGrpc.EdgeRpcServiceImplBase() {});
+
+ edgeGrpcService.setupSsl(builder);
+
+ return builder.build().start();
+ }
+
+ private void assertTlsConnectivity(X509Certificate trustedCert) throws Exception {
+ String certPem = toPem(trustedCert);
+ var clientSsl = GrpcSslContexts.forClient()
+ .trustManager(new ByteArrayInputStream(certPem.getBytes(StandardCharsets.UTF_8)))
+ .build();
+
+ channel = NettyChannelBuilder.forAddress("localhost", server.getPort())
+ .sslContext(clientSsl)
+ .build();
+
+ channel.getState(true); // trigger connection attempt
+ await().atMost(AbstractWebTest.TIMEOUT, TimeUnit.SECONDS)
+ .pollInterval(50, TimeUnit.MILLISECONDS)
+ .untilAsserted(() -> {
+ var state = channel.getState(false);
+ if (state == io.grpc.ConnectivityState.TRANSIENT_FAILURE) {
+ throw new AssertionError("TLS handshake failed: channel in TRANSIENT_FAILURE");
+ }
+ assertThat(state).isEqualTo(io.grpc.ConnectivityState.READY);
+ });
+ }
+
+ // --- Cert/key generation ---
+
+ private X509Certificate generateSelfSignedCert(KeyPair kp, String sigAlg) throws Exception {
+ X500Name subject = new X500Name("CN=localhost");
+ Date now = new Date();
+ return new JcaX509CertificateConverter().getCertificate(
+ new JcaX509v3CertificateBuilder(
+ subject, BigInteger.ONE, now,
+ new Date(now.getTime() + TimeUnit.DAYS.toMillis(1)),
+ subject, kp.getPublic())
+ .build(new JcaContentSignerBuilder(sigAlg).build(kp.getPrivate())));
+ }
+
+ // --- PEM file helpers ---
+
+ private String toPem(Object obj) throws Exception {
+ java.io.StringWriter sw = new java.io.StringWriter();
+ try (JcaPEMWriter w = new JcaPEMWriter(sw)) {
+ w.writeObject(obj);
+ }
+ return sw.toString();
+ }
+
+ private Path writeTempPem(String prefix, Object... objects) throws Exception {
+ Path p = Files.createTempFile(prefix + "-", ".pem");
+ tempFiles.add(p);
+ try (JcaPEMWriter w = new JcaPEMWriter(Files.newBufferedWriter(p))) {
+ for (Object o : objects) {
+ w.writeObject(toPkcs8IfKey(o));
+ }
+ }
+ return p;
+ }
+
+ private Path writeTempPemEncrypted(String prefix, String password, Object... objects) throws Exception {
+ Path p = Files.createTempFile(prefix + "-", ".pem");
+ tempFiles.add(p);
+ var encryptor = new JcePEMEncryptorBuilder("AES-256-CBC")
+ .setProvider(BouncyCastleProvider.PROVIDER_NAME)
+ .build(password.toCharArray());
+ try (JcaPEMWriter w = new JcaPEMWriter(Files.newBufferedWriter(p))) {
+ for (Object o : objects) {
+ if (o instanceof PrivateKey) {
+ w.writeObject(o, encryptor);
+ } else {
+ w.writeObject(o);
+ }
+ }
+ }
+ return p;
+ }
+
+ private Object toPkcs8IfKey(Object o) {
+ if (o instanceof PrivateKey pk) {
+ return new PemObject("PRIVATE KEY", pk.getEncoded());
+ }
+ return o;
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
index c54ffe4a07..05ab1165f6 100644
--- a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
@@ -115,6 +115,8 @@ import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -1749,13 +1751,13 @@ public class EntityServiceTest extends AbstractControllerTest {
}
@Test
- public void testFindTenantTelemetry() {
+ public void testFindTenantTelemetry() throws ExecutionException, InterruptedException, TimeoutException {
// save timeseries by sys admin
BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, new DoubleDataEntry("temperature", 45.5));
- timeseriesService.save(TenantId.SYS_TENANT_ID, tenantId, timeseries);
+ timeseriesService.save(TenantId.SYS_TENANT_ID, tenantId, timeseries).get(TIMEOUT, TimeUnit.SECONDS);
AttributeKvEntry attr = new BaseAttributeKvEntry(new LongDataEntry("attr", 10L), 42L);
- attributesService.save(TenantId.SYS_TENANT_ID, tenantId, SERVER_SCOPE, List.of(attr));
+ attributesService.save(TenantId.SYS_TENANT_ID, tenantId, SERVER_SCOPE, List.of(attr)).get(TIMEOUT, TimeUnit.SECONDS);
SingleEntityFilter singleEntityFilter = new SingleEntityFilter();
singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantId));
diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java
index cf37afad5c..0c12767b60 100644
--- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java
@@ -23,8 +23,8 @@ import org.junit.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
@@ -127,10 +127,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
})
public class HousekeeperServiceTest extends AbstractControllerTest {
- @SpyBean
+ @MockitoSpyBean
private HousekeeperService housekeeperService;
- @SpyBean
+ @MockitoSpyBean
private HousekeeperReprocessingService housekeeperReprocessingService;
+ @MockitoSpyBean
+ private TsHistoryDeletionTaskProcessor tsHistoryDeletionTaskProcessor;
@Autowired
private EventService eventService;
@Autowired
@@ -153,8 +155,6 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
private CustomerService customerService;
@Autowired
private DashboardService dashboardService;
- @SpyBean
- private TsHistoryDeletionTaskProcessor tsHistoryDeletionTaskProcessor;
private TenantId tenantId;
diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
index f5607415fa..cf99669777 100644
--- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
+++ b/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);
diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
index e3342dbf43..94af26d3d7 100644
--- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
@@ -211,7 +211,6 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notificationRule.setTriggerConfig(triggerConfig);
EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig();
- recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM);
Map> escalationTable = new HashMap<>();
recipientsConfig.setEscalationTable(escalationTable);
Map clients = new HashMap<>();
@@ -329,7 +328,6 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notificationRule.setTriggerConfig(triggerConfig);
EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig();
- recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM);
Map> escalationTable = new HashMap<>();
recipientsConfig.setEscalationTable(escalationTable);
@@ -640,8 +638,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();
rule.setTriggerConfig(triggerConfig);
- DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig();
- recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
+ DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
recipientsConfig.setTargets(List.of(createNotificationTarget(tenantAdminUserId).getUuidId()));
rule.setRecipientsConfig(recipientsConfig);
rule = saveNotificationRule(rule);
@@ -671,8 +668,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
triggerConfig.setCreated(true);
rule.setTriggerConfig(triggerConfig);
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
- DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig();
- recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
+ DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
recipientsConfig.setTargets(List.of(target.getUuidId()));
rule.setRecipientsConfig(recipientsConfig);
rule = saveNotificationRule(rule);
diff --git a/application/src/test/java/org/thingsboard/server/service/ttl/NotificationsCleanUpServiceTest.java b/application/src/test/java/org/thingsboard/server/service/ttl/NotificationsCleanUpServiceTest.java
new file mode 100644
index 0000000000..d74652325a
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/ttl/NotificationsCleanUpServiceTest.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright © 2016-2026 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.ttl;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
+import org.thingsboard.server.dao.notification.NotificationRequestDao;
+import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.queue.discovery.PartitionService;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class NotificationsCleanUpServiceTest {
+
+ @Mock
+ private PartitionService partitionService;
+ @Mock
+ private SqlPartitioningRepository partitioningRepository;
+ @Mock
+ private NotificationRequestDao notificationRequestDao;
+ @Mock
+ private TenantService tenantService;
+
+ private NotificationsCleanUpService cleanUpService;
+
+ private static final int BATCH_SIZE = 3;
+
+ @BeforeEach
+ public void setUp() {
+ cleanUpService = new NotificationsCleanUpService(partitionService, partitioningRepository, notificationRequestDao, tenantService);
+ ReflectionTestUtils.setField(cleanUpService, "ttlInSec", 2592000L);
+ ReflectionTestUtils.setField(cleanUpService, "partitionSizeInHours", 168);
+ ReflectionTestUtils.setField(cleanUpService, "removalBatchSize", BATCH_SIZE);
+ }
+
+ @Test
+ public void testBatchLoopCallsDaoMultipleTimes() {
+ TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
+ when(partitionService.resolve(any(), any(), any())).thenReturn(myPartition);
+ when(partitioningRepository.dropPartitionsBefore(anyString(), anyLong(), anyLong()))
+ .thenReturn(System.currentTimeMillis());
+
+ TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
+ when(tenantService.findTenantsIds(any()))
+ .thenReturn(new PageData<>(List.of(tenantId), 1, 1, false));
+
+ // Sysadmin: returns 3 (full batch), then 1 (partial) -> 2 calls
+ when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE)))
+ .thenReturn(BATCH_SIZE)
+ .thenReturn(1);
+ // Tenant: returns 3, 3, 0 -> 3 calls
+ when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE)))
+ .thenReturn(BATCH_SIZE)
+ .thenReturn(BATCH_SIZE)
+ .thenReturn(0);
+
+ cleanUpService.cleanUp();
+
+ verify(notificationRequestDao, times(2))
+ .removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE));
+ verify(notificationRequestDao, times(3))
+ .removeByTenantIdAndCreatedTimeBeforeBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE));
+ }
+
+ @Test
+ public void testSkipsTenantNotOnMyPartition() {
+ TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
+ TopicPartitionInfo notMyPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(false).build();
+ when(partitionService.resolve(any(), eq(TenantId.SYS_TENANT_ID), eq(TenantId.SYS_TENANT_ID)))
+ .thenReturn(myPartition);
+ when(partitioningRepository.dropPartitionsBefore(anyString(), anyLong(), anyLong()))
+ .thenReturn(System.currentTimeMillis());
+
+ // Sysadmin: no records
+ when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE)))
+ .thenReturn(0);
+
+ TenantId myTenant = TenantId.fromUUID(UUID.randomUUID());
+ TenantId otherTenant = TenantId.fromUUID(UUID.randomUUID());
+ when(tenantService.findTenantsIds(any()))
+ .thenReturn(new PageData<>(List.of(myTenant, otherTenant), 2, 1, false));
+ when(partitionService.resolve(any(), eq(myTenant), eq(myTenant))).thenReturn(myPartition);
+ when(partitionService.resolve(any(), eq(otherTenant), eq(otherTenant))).thenReturn(notMyPartition);
+
+ when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE)))
+ .thenReturn(0);
+
+ cleanUpService.cleanUp();
+
+ verify(notificationRequestDao).removeByTenantIdAndCreatedTimeBeforeBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE));
+ verify(notificationRequestDao, never()).removeByTenantIdAndCreatedTimeBeforeBatch(eq(otherTenant), anyLong(), anyInt());
+ }
+
+ @Test
+ public void testNoPartitionsDropped_stillCleansUpRequests() {
+ TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
+ when(partitionService.resolve(any(), any(), any())).thenReturn(myPartition);
+ when(partitioningRepository.dropPartitionsBefore(anyString(), anyLong(), anyLong()))
+ .thenReturn(0L);
+
+ when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE)))
+ .thenReturn(0);
+ when(tenantService.findTenantsIds(any()))
+ .thenReturn(new PageData<>(List.of(), 0, 0, false));
+
+ cleanUpService.cleanUp();
+
+ verify(notificationRequestDao).removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE));
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpServiceTest.java b/application/src/test/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpServiceTest.java
new file mode 100644
index 0000000000..22cc1be1dd
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpServiceTest.java
@@ -0,0 +1,136 @@
+/**
+ * Copyright © 2016-2026 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.ttl.rpc;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.thingsboard.server.common.data.TenantProfile;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
+import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
+import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
+import org.thingsboard.server.dao.rpc.RpcDao;
+import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.queue.discovery.PartitionService;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class RpcCleanUpServiceTest {
+
+ @Mock
+ private PartitionService partitionService;
+ @Mock
+ private RpcDao rpcDao;
+ @Mock
+ private TenantService tenantService;
+ @Mock
+ private TbTenantProfileCache tenantProfileCache;
+
+ private RpcCleanUpService cleanUpService;
+
+ private static final int BATCH_SIZE = 3;
+
+ @BeforeEach
+ public void setUp() {
+ cleanUpService = new RpcCleanUpService(tenantService, partitionService, tenantProfileCache, rpcDao);
+ ReflectionTestUtils.setField(cleanUpService, "removalBatchSize", BATCH_SIZE);
+ }
+
+ @Test
+ public void testBatchLoopCallsDaoMultipleTimes() {
+ TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
+ setupTenant(tenantId, 7);
+
+ // Returns 3 (full batch), 3 (full batch), 1 (partial) -> 3 calls
+ when(rpcDao.deleteOutdatedRpcByTenantIdBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE)))
+ .thenReturn(BATCH_SIZE)
+ .thenReturn(BATCH_SIZE)
+ .thenReturn(1);
+
+ cleanUpService.cleanUp();
+
+ verify(rpcDao, times(3)).deleteOutdatedRpcByTenantIdBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE));
+ }
+
+ @Test
+ public void testSkipsTenantNotOnMyPartition() {
+ TenantId myTenant = TenantId.fromUUID(UUID.randomUUID());
+ TenantId otherTenant = TenantId.fromUUID(UUID.randomUUID());
+
+ TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
+ TopicPartitionInfo notMyPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(false).build();
+
+ when(tenantService.findTenantsIds(any()))
+ .thenReturn(new PageData<>(List.of(myTenant, otherTenant), 2, 1, false));
+ when(partitionService.resolve(any(), eq(myTenant), eq(myTenant))).thenReturn(myPartition);
+ when(partitionService.resolve(any(), eq(otherTenant), eq(otherTenant))).thenReturn(notMyPartition);
+
+ setupTenantProfile(myTenant, 7);
+ when(rpcDao.deleteOutdatedRpcByTenantIdBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE)))
+ .thenReturn(0);
+
+ cleanUpService.cleanUp();
+
+ verify(rpcDao).deleteOutdatedRpcByTenantIdBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE));
+ verify(rpcDao, never()).deleteOutdatedRpcByTenantIdBatch(eq(otherTenant), anyLong(), anyInt());
+ }
+
+ @Test
+ public void testSkipsTenantWithZeroTtl() {
+ TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
+ setupTenant(tenantId, 0);
+
+ cleanUpService.cleanUp();
+
+ verify(rpcDao, never()).deleteOutdatedRpcByTenantIdBatch(any(), anyLong(), anyInt());
+ }
+
+ private void setupTenant(TenantId tenantId, int rpcTtlDays) {
+ TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
+ when(partitionService.resolve(any(), eq(tenantId), eq(tenantId))).thenReturn(myPartition);
+ when(tenantService.findTenantsIds(any()))
+ .thenReturn(new PageData<>(List.of(tenantId), 1, 1, false));
+ setupTenantProfile(tenantId, rpcTtlDays);
+ }
+
+ private void setupTenantProfile(TenantId tenantId, int rpcTtlDays) {
+ TenantProfile profile = new TenantProfile();
+ TenantProfileData profileData = new TenantProfileData();
+ DefaultTenantProfileConfiguration config = new DefaultTenantProfileConfiguration();
+ config.setRpcTtlDays(rpcTtlDays);
+ profileData.setConfiguration(config);
+ profile.setProfileData(profileData);
+ when(tenantProfileCache.get(tenantId)).thenReturn(profile);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java
index 6a7d631eb1..35e12691c9 100644
--- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java
+++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java
@@ -171,7 +171,14 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
if (this.leshanClient != null) {
log.info("Stop/reboot LwM2M client {}", this.leshanClient.getEndpoint(identity));
- this.leshanClient.stop(false);
+ try {
+ this.leshanClient.stop(false);
+ } catch (Exception stopEx) {
+ // Leshan may throw NPE during CoAP observe-relation cleanup when the server
+ // reference is null (race condition in NotificationDataStore.toKey()).
+ // The client is still considered stopped at this point — proceed with restart.
+ log.warn("Exception during LwM2M client stop, proceeding with restart: {}", stopEx.getMessage());
+ }
log.info("Start after update fw LwM2M client {}", this.leshanClient.getEndpoint(identity));
this.leshanClient.start();
diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java
index 432fac6d1a..c61ca0b123 100644
--- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java
@@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
+import org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -37,6 +38,12 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID
@Slf4j
public class RpcLwm2mIntegrationReadCollectedValueTest extends AbstractRpcLwM2MIntegrationTest {
+ @Before
+ public void resetCollectedValueTimestamps() {
+ Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0 = 0;
+ Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1 = 0;
+ }
+
/**
* Read {"id":"/3303/12/5700"}
* Trigger a Send operation from the client with multiple values for the same resource as a payload
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/pat/ApiKeyService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/pat/ApiKeyService.java
index 798549cf85..cdb8c23c41 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/pat/ApiKeyService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/pat/ApiKeyService.java
@@ -24,10 +24,14 @@ import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.pat.ApiKeyInfo;
import org.thingsboard.server.dao.entity.EntityDaoService;
+import java.util.List;
+
public interface ApiKeyService extends EntityDaoService {
ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKey);
+ ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKeyInfo, String value, boolean doValidate);
+
void deleteApiKey(TenantId tenantId, ApiKey apiKey, boolean force);
void deleteByUserId(TenantId tenantId, UserId userId);
@@ -38,4 +42,8 @@ public interface ApiKeyService extends EntityDaoService {
PageData findApiKeysByUserId(TenantId tenantId, UserId userId, PageLink pageLink);
+ List findApiKeysByUserId(TenantId tenantId, UserId userId);
+
+ PageData findApiKeysByTenantId(TenantId tenantId, PageLink pageLink);
+
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java
index e9e1658731..2c65a3dfc8 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java
@@ -33,7 +33,7 @@ import java.util.Optional;
import java.util.stream.Collectors;
@EqualsAndHashCode(callSuper = true)
-@JsonPropertyOrder({"title", "image", "mobileHide", "mobileOrder", "configuration", "name", "resources"})
+@JsonPropertyOrder({"id", "createdTime", "tenantId", "title", "name", "image", "mobileHide", "mobileOrder", "assignedCustomers", "configuration", "resources", "version"})
public class Dashboard extends DashboardInfo implements ExportableEntity {
private static final long serialVersionUID = 872682138346187503L;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java
index 2a3b95ba0c..92a446fcb6 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java
@@ -15,13 +15,29 @@
*/
package org.thingsboard.server.common.data;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
+@JsonPropertyOrder({
+ "emailEnabled",
+ "smsEnabled",
+ "notificationEnabled",
+ "oauthEnabled",
+ "twoFaEnabled"
+})
+@Schema
@Data
public class FeaturesInfo {
+ @JsonProperty("emailEnabled")
boolean isEmailEnabled;
+ @JsonProperty("smsEnabled")
boolean isSmsEnabled;
+ @JsonProperty("notificationEnabled")
boolean isNotificationEnabled;
+ @JsonProperty("oauthEnabled")
boolean isOauthEnabled;
+ @JsonProperty("twoFaEnabled")
boolean isTwoFaEnabled;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java
index 930e4dddca..dbed1026a2 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data;
+import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -39,9 +40,15 @@ public class ShortCustomerInfo {
@NoXss
private String title;
+ @JsonProperty("isPublic")
@Schema(description = "Indicates special 'Public' customer used to embed dashboards on public websites.")
- @Getter @Setter
- private boolean isPublic;
+ private boolean publicCustomer;
+
+ @JsonProperty("isPublic")
+ public boolean isPublic() { return publicCustomer; }
+
+ @JsonProperty("isPublic")
+ public void setPublic(boolean publicCustomer) { this.publicCustomer = publicCustomer; }
@Override
public boolean equals(Object o) {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java
index 73bd3cddc4..3fd86d1163 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data;
+import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -23,6 +24,7 @@ import java.util.List;
@Data
public class SystemInfo {
@Schema(description = "Is monolith.")
+ @JsonProperty("monolith")
private boolean isMonolith;
@Schema(description = "System data.")
private List systemData;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
index abd1515ff2..9f9bdec49d 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
@@ -84,6 +84,7 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha
@Schema(description = "Timestamp of the alarm assignment, in milliseconds", example = "1634115928465")
private long assignTs;
@Schema(description = "JSON object with alarm details")
+ @JsonProperty
private transient JsonNode details;
@Schema(description = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true")
private boolean propagate;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java
index 021639bc8c..2d3928ee88 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java
@@ -17,15 +17,15 @@ package org.thingsboard.server.common.data.cf;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter;
+import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.Setter;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.HasAdditionalInfo;
import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
@@ -39,7 +39,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
-import java.io.Serial;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
@@ -48,10 +47,7 @@ import java.util.Set;
@Schema
@Data
@EqualsAndHashCode(callSuper = true)
-public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings {
-
- @Serial
- private static final long serialVersionUID = 4491966747773381420L;
+public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings, HasAdditionalInfo {
public static final Map> SUPPORTED_ENTITIES = Map.of(
EntityType.DEVICE, CalculatedFieldType.all,
@@ -90,28 +86,17 @@ public class CalculatedField extends BaseData implements HasN
@Valid
@NotNull
private CalculatedFieldConfiguration configuration;
- @Getter
- @Setter
private Long version;
+ @NoXss
+ @Schema(description = "Additional parameters of the calculated field")
+ private JsonNode additionalInfo;
- public CalculatedField() {
- super();
- }
+ public CalculatedField() {}
public CalculatedField(CalculatedFieldId id) {
super(id);
}
- public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version) {
- this.tenantId = tenantId;
- this.entityId = entityId;
- this.type = type;
- this.name = name;
- this.configurationVersion = configurationVersion;
- this.configuration = configuration;
- this.version = version;
- }
-
public CalculatedField(CalculatedField calculatedField) {
super(calculatedField);
this.tenantId = calculatedField.tenantId;
@@ -123,6 +108,7 @@ public class CalculatedField extends BaseData implements HasN
this.configurationVersion = calculatedField.configurationVersion;
this.configuration = calculatedField.configuration;
this.version = calculatedField.version;
+ this.additionalInfo = calculatedField.additionalInfo;
}
@Schema(description = "JSON object with the Calculated Field Id. Referencing non-existing Calculated Field Id will cause error.")
@@ -159,6 +145,7 @@ public class CalculatedField extends BaseData implements HasN
.append(", name='").append(name)
.append(", configurationVersion=").append(configurationVersion)
.append(", configuration=").append(configuration)
+ .append(", additionalInfo=").append(additionalInfo)
.append(", version=").append(version)
.append(", createdTime=").append(createdTime)
.append(", id=").append(id).append(']')
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
index 834e19a1c2..474a0270c1 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
@@ -48,7 +48,8 @@ public enum EdgeEventType {
OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT),
DOMAIN(true, EntityType.DOMAIN),
CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD),
- AI_MODEL(true, EntityType.AI_MODEL);
+ AI_MODEL(true, EntityType.AI_MODEL),
+ API_KEY(true, EntityType.API_KEY);
private final boolean allEdgesRelated;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java
index 8779416106..01463dade8 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.event;
+import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.thingsboard.server.common.data.StringUtils;
@@ -26,10 +27,17 @@ public abstract class DebugEventFilter implements EventFilter {
@Schema(description = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152")
protected String server;
@Schema(description = "Boolean value to filter the errors", allowableValues = {"false", "true"})
+ @JsonProperty("isError")
protected boolean isError;
@Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB")
protected String errorStr;
+ @JsonProperty("isError")
+ public boolean isError() {
+ return isError;
+ }
+
+ @JsonProperty("isError")
public void setIsError(boolean isError) {
this.isError = isError;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java
index dea590295e..d66ec046de 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java
@@ -23,6 +23,7 @@ import lombok.ToString;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
+import java.io.Serial;
import java.util.List;
import java.util.UUID;
@@ -32,6 +33,9 @@ import java.util.UUID;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AlarmsDeletionHousekeeperTask extends HousekeeperTask {
+ @Serial
+ private static final long serialVersionUID = 9214680001573764374L;
+
private List alarms;
public AlarmsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId) {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java
index 0313190056..445850d387 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java
@@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
+import java.io.Serial;
import java.util.List;
import java.util.UUID;
@@ -33,6 +34,9 @@ import java.util.UUID;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AlarmsUnassignHousekeeperTask extends HousekeeperTask {
+ @Serial
+ private static final long serialVersionUID = 9156667024462937756L;
+
private String userTitle;
private List alarms;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java
index fe25a98a1d..c1a233b1c4 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java
@@ -23,6 +23,7 @@ import lombok.ToString;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
+import java.io.Serial;
import java.util.List;
import java.util.UUID;
@@ -32,6 +33,9 @@ import java.util.UUID;
@NoArgsConstructor
public class EntitiesDeletionHousekeeperTask extends HousekeeperTask {
+ @Serial
+ private static final long serialVersionUID = 9009068831061529286L;
+
private EntityType entityType;
private List entities;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java
index 875ef2765f..2df7cf4dd4 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java
@@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
+import java.io.Serial;
import java.io.Serializable;
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -45,6 +46,9 @@ import java.io.Serializable;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class HousekeeperTask implements Serializable {
+ @Serial
+ private static final long serialVersionUID = -2585974110832225152L;
+
private TenantId tenantId;
private EntityId entityId;
private HousekeeperTaskType taskType;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java
index cd3e94e5c6..931c2931cb 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java
@@ -23,12 +23,17 @@ import lombok.ToString;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
+import java.io.Serial;
+
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LatestTsDeletionHousekeeperTask extends HousekeeperTask {
+ @Serial
+ private static final long serialVersionUID = 5193191938513490138L;
+
private String key;
public LatestTsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, String key) {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TenantEntitiesDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TenantEntitiesDeletionHousekeeperTask.java
index 443d929d8b..be7ff6f7ec 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TenantEntitiesDeletionHousekeeperTask.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TenantEntitiesDeletionHousekeeperTask.java
@@ -23,12 +23,17 @@ import lombok.ToString;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
+import java.io.Serial;
+
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
public class TenantEntitiesDeletionHousekeeperTask extends HousekeeperTask {
+ @Serial
+ private static final long serialVersionUID = -8033108795318393447L;
+
private EntityType entityType;
public TenantEntitiesDeletionHousekeeperTask(TenantId tenantId, EntityType entityType) {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java
index d9315f0ff4..b520899ca4 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java
@@ -23,12 +23,17 @@ import lombok.ToString;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
+import java.io.Serial;
+
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TsHistoryDeletionHousekeeperTask extends HousekeeperTask {
+ @Serial
+ private static final long serialVersionUID = 4573851542705079043L;
+
private String key;
public TsHistoryDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, String key) {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java
index da7ade45e1..5923b8c02c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class AdminSettingsId extends UUIDBased implements EntityId {
@Serial
@@ -33,7 +34,7 @@ public class AdminSettingsId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ADMIN_SETTINGS", allowableValues = "ADMIN_SETTINGS")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ADMIN_SETTINGS", allowableValues = "ADMIN_SETTINGS")
@Override
public EntityType getEntityType() {
return EntityType.ADMIN_SETTINGS;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java
index dbb8b25635..70476564d5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public final class AiModelId extends UUIDBased implements EntityId {
@Serial
@@ -36,6 +37,7 @@ public final class AiModelId extends UUIDBased implements EntityId {
@Override
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
+ accessMode = Schema.AccessMode.READ_ONLY,
description = "Entity type of the AI model",
example = "AI_MODEL",
allowableValues = "AI_MODEL"
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java
index 444492c328..db2c63d614 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java
@@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
-@Schema
+@Schema(allOf = EntityId.class)
public class AlarmId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -36,7 +36,7 @@ public class AlarmId extends UUIDBased implements EntityId {
return new AlarmId(UUID.fromString(alarmId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ALARM", allowableValues = "ALARM")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ALARM", allowableValues = "ALARM")
@Override
public EntityType getEntityType() {
return EntityType.ALARM;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java
index 514c29665c..186dcaca40 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class ApiKeyId extends UUIDBased implements EntityId {
@Serial
@@ -38,7 +39,7 @@ public class ApiKeyId extends UUIDBased implements EntityId {
}
@Override
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "API_KEY", allowableValues = "API_KEY")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "API_KEY", allowableValues = "API_KEY")
public EntityType getEntityType() {
return EntityType.API_KEY;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java
index 78e5c90bee..02d6d387a2 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java
@@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
-@Schema
+@Schema(allOf = EntityId.class)
public class ApiUsageStateId extends UUIDBased implements EntityId {
@JsonCreator
@@ -34,7 +34,7 @@ public class ApiUsageStateId extends UUIDBased implements EntityId {
return new ApiUsageStateId(UUID.fromString(userId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE")
@Override
public EntityType getEntityType() {
return EntityType.API_USAGE_STATE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java
index dc42fffeb5..2d57cc365d 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java
@@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
-@Schema
+@Schema(allOf = EntityId.class)
public class AssetId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -36,7 +36,7 @@ public class AssetId extends UUIDBased implements EntityId {
return new AssetId(UUID.fromString(assetId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ASSET", allowableValues = "ASSET")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ASSET", allowableValues = "ASSET")
@Override
public EntityType getEntityType() {
return EntityType.ASSET;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java
index f4f7a3de70..981f381a4a 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class AssetProfileId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -35,7 +36,7 @@ public class AssetProfileId extends UUIDBased implements EntityId {
return new AssetProfileId(UUID.fromString(assetProfileId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE")
@Override
public EntityType getEntityType() {
return EntityType.ASSET_PROFILE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java
index 5168515cd8..094df0e935 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java
@@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID;
-@Schema
+@Schema(allOf = EntityId.class)
public class CalculatedFieldId extends UUIDBased implements EntityId {
@Serial
@@ -38,7 +38,7 @@ public class CalculatedFieldId extends UUIDBased implements EntityId {
return new CalculatedFieldId(UUID.fromString(calculatedFieldId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CALCULATED_FIELD", allowableValues = "CALCULATED_FIELD")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "CALCULATED_FIELD", allowableValues = "CALCULATED_FIELD")
@Override
public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java
index a3b1e28147..4de326f26c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java
@@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
-@Schema
+@Schema(allOf = EntityId.class)
public final class CustomerId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -32,7 +32,7 @@ public final class CustomerId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CUSTOMER", allowableValues = "CUSTOMER")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "CUSTOMER", allowableValues = "CUSTOMER")
@Override
public EntityType getEntityType() {
return EntityType.CUSTOMER;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java
index bd326257a6..c98e70177e 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java
@@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
-@Schema
+@Schema(allOf = EntityId.class)
public class DashboardId extends UUIDBased implements EntityId {
@JsonCreator
@@ -34,7 +34,7 @@ public class DashboardId extends UUIDBased implements EntityId {
return new DashboardId(UUID.fromString(dashboardId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DASHBOARD", allowableValues = "DASHBOARD")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DASHBOARD", allowableValues = "DASHBOARD")
@Override
public EntityType getEntityType() {
return EntityType.DASHBOARD;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java
index 7dd840815b..a7b9c12e19 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java
@@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
-@Schema
+@Schema(allOf = EntityId.class)
public class DeviceId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -37,7 +37,7 @@ public class DeviceId extends UUIDBased implements EntityId {
}
@Override
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DEVICE", allowableValues = "DEVICE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DEVICE", allowableValues = "DEVICE")
public EntityType getEntityType() {
return EntityType.DEVICE;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java
index 2adba3640c..dea0dd2a65 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class DeviceProfileId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -35,7 +36,7 @@ public class DeviceProfileId extends UUIDBased implements EntityId {
return new DeviceProfileId(UUID.fromString(deviceProfileId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE")
@Override
public EntityType getEntityType() {
return EntityType.DEVICE_PROFILE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java
index 0768eec45e..223ea8d8b5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java
@@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class DomainId extends UUIDBased implements EntityId {
@JsonCreator
@@ -32,6 +34,7 @@ public class DomainId extends UUIDBased implements EntityId {
return new DomainId(UUID.fromString(oauth2DomainId));
}
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DOMAIN", allowableValues = "DOMAIN")
@Override
public EntityType getEntityType() {
return EntityType.DOMAIN;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java
index 05019a608f..45a95d6632 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class EdgeId extends UUIDBased implements EntityId {
@Serial
@@ -43,7 +44,7 @@ public class EdgeId extends UUIDBased implements EntityId {
return new EdgeId(UUID.fromString(edgeId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "EDGE", allowableValues = "EDGE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "EDGE", allowableValues = "EDGE")
@Override
public EntityType getEntityType() {
return EntityType.EDGE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java
index c57dc79514..c96b4c38d5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
@@ -26,7 +27,47 @@ import java.util.UUID;
@JsonDeserialize(using = EntityIdDeserializer.class)
@JsonSerialize(using = EntityIdSerializer.class)
-@Schema
+@Schema(
+ discriminatorProperty = "entityType",
+ discriminatorMapping = {
+ @DiscriminatorMapping(value = "ADMIN_SETTINGS", schema = AdminSettingsId.class),
+ @DiscriminatorMapping(value = "AI_MODEL", schema = AiModelId.class),
+ @DiscriminatorMapping(value = "ALARM", schema = AlarmId.class),
+ @DiscriminatorMapping(value = "API_KEY", schema = ApiKeyId.class),
+ @DiscriminatorMapping(value = "API_USAGE_STATE", schema = ApiUsageStateId.class),
+ @DiscriminatorMapping(value = "ASSET", schema = AssetId.class),
+ @DiscriminatorMapping(value = "ASSET_PROFILE", schema = AssetProfileId.class),
+ @DiscriminatorMapping(value = "CALCULATED_FIELD", schema = CalculatedFieldId.class),
+ @DiscriminatorMapping(value = "CUSTOMER", schema = CustomerId.class),
+ @DiscriminatorMapping(value = "DASHBOARD", schema = DashboardId.class),
+ @DiscriminatorMapping(value = "DEVICE", schema = DeviceId.class),
+ @DiscriminatorMapping(value = "DEVICE_PROFILE", schema = DeviceProfileId.class),
+ @DiscriminatorMapping(value = "DOMAIN", schema = DomainId.class),
+ @DiscriminatorMapping(value = "EDGE", schema = EdgeId.class),
+ @DiscriminatorMapping(value = "ENTITY_VIEW", schema = EntityViewId.class),
+ @DiscriminatorMapping(value = "JOB", schema = JobId.class),
+ @DiscriminatorMapping(value = "MOBILE_APP", schema = MobileAppId.class),
+ @DiscriminatorMapping(value = "MOBILE_APP_BUNDLE", schema = MobileAppBundleId.class),
+ @DiscriminatorMapping(value = "NOTIFICATION", schema = NotificationId.class),
+ @DiscriminatorMapping(value = "NOTIFICATION_REQUEST", schema = NotificationRequestId.class),
+ @DiscriminatorMapping(value = "NOTIFICATION_RULE", schema = NotificationRuleId.class),
+ @DiscriminatorMapping(value = "NOTIFICATION_TARGET", schema = NotificationTargetId.class),
+ @DiscriminatorMapping(value = "NOTIFICATION_TEMPLATE", schema = NotificationTemplateId.class),
+ @DiscriminatorMapping(value = "OAUTH2_CLIENT", schema = OAuth2ClientId.class),
+ @DiscriminatorMapping(value = "OTA_PACKAGE", schema = OtaPackageId.class),
+ @DiscriminatorMapping(value = "QUEUE", schema = QueueId.class),
+ @DiscriminatorMapping(value = "QUEUE_STATS", schema = QueueStatsId.class),
+ @DiscriminatorMapping(value = "RPC", schema = RpcId.class),
+ @DiscriminatorMapping(value = "RULE_CHAIN", schema = RuleChainId.class),
+ @DiscriminatorMapping(value = "RULE_NODE", schema = RuleNodeId.class),
+ @DiscriminatorMapping(value = "TB_RESOURCE", schema = TbResourceId.class),
+ @DiscriminatorMapping(value = "TENANT", schema = TenantId.class),
+ @DiscriminatorMapping(value = "TENANT_PROFILE", schema = TenantProfileId.class),
+ @DiscriminatorMapping(value = "USER", schema = UserId.class),
+ @DiscriminatorMapping(value = "WIDGETS_BUNDLE", schema = WidgetsBundleId.class),
+ @DiscriminatorMapping(value = "WIDGET_TYPE", schema = WidgetTypeId.class)
+ }
+)
public interface EntityId extends HasUUID, Serializable { //NOSONAR, the constant is closely related to EntityId
UUID NULL_UUID = UUID.fromString("13814000-1dd2-11b2-8080-808080808080");
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
index 7baa8e72f2..ae033f769c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
@@ -114,6 +114,7 @@ public class EntityIdFactory {
case DOMAIN -> new DomainId(uuid);
case CALCULATED_FIELD -> new CalculatedFieldId(uuid);
case AI_MODEL -> new AiModelId(uuid);
+ case API_KEY -> new ApiKeyId(uuid);
case ADMIN_SETTINGS -> new AdminSettingsId(uuid);
default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!");
};
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java
index cd5b0164db..8bd357a4fc 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java
@@ -25,6 +25,7 @@ import java.util.UUID;
/**
* Created by Victor Basanets on 8/27/2017.
*/
+@Schema(allOf = EntityId.class)
public class EntityViewId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -38,7 +39,7 @@ public class EntityViewId extends UUIDBased implements EntityId {
return new EntityViewId(UUID.fromString(entityViewID));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW")
@Override
public EntityType getEntityType() {
return EntityType.ENTITY_VIEW;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java
index 51e6b8e539..2d369e8b71 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class JobId extends UUIDBased implements EntityId {
@Serial
@@ -33,7 +34,7 @@ public class JobId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "JOB", allowableValues = "JOB")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "JOB", allowableValues = "JOB")
@Override
public EntityType getEntityType() {
return EntityType.JOB;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java
index 6a9c11207a..cfd0fd109f 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java
@@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class MobileAppBundleId extends UUIDBased implements EntityId{
@JsonCreator
@@ -32,6 +34,7 @@ public class MobileAppBundleId extends UUIDBased implements EntityId{
return new MobileAppBundleId(UUID.fromString(mobileAppId));
}
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "MOBILE_APP_BUNDLE", allowableValues = "MOBILE_APP_BUNDLE")
@Override
public EntityType getEntityType() {
return EntityType.MOBILE_APP_BUNDLE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java
index f19e52cc46..bf3571bf7e 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java
@@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class MobileAppId extends UUIDBased implements EntityId{
@JsonCreator
@@ -32,6 +34,7 @@ public class MobileAppId extends UUIDBased implements EntityId{
return new MobileAppId(UUID.fromString(mobileAppId));
}
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "MOBILE_APP", allowableValues = "MOBILE_APP")
@Override
public EntityType getEntityType() {
return EntityType.MOBILE_APP;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java
index 3ba6bf4ca4..a702648a52 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class NotificationId extends UUIDBased implements EntityId {
@JsonCreator
@@ -29,7 +30,7 @@ public class NotificationId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION")
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java
index 5ff42bc0e2..3caa5f1f8c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class NotificationRequestId extends UUIDBased implements EntityId {
@JsonCreator
@@ -29,7 +30,7 @@ public class NotificationRequestId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST")
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_REQUEST;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java
index ff57e4d252..39e2ccf54c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class NotificationRuleId extends UUIDBased implements EntityId {
@JsonCreator
@@ -29,7 +30,7 @@ public class NotificationRuleId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE")
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_RULE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java
index 9287de6248..593ce1c1aa 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java
@@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
-@Schema
+@Schema(allOf = EntityId.class)
public class NotificationTargetId extends UUIDBased implements EntityId {
@JsonCreator
@@ -30,7 +30,7 @@ public class NotificationTargetId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET")
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_TARGET;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java
index ae2ea582fd..5a1635044a 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class NotificationTemplateId extends UUIDBased implements EntityId {
@JsonCreator
@@ -29,7 +30,7 @@ public class NotificationTemplateId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE")
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_TEMPLATE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java
index 9ef5871135..491643b921 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java
@@ -17,10 +17,12 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class OAuth2ClientId extends UUIDBased implements EntityId {
@JsonCreator
@@ -32,6 +34,7 @@ public class OAuth2ClientId extends UUIDBased implements EntityId {
return new OAuth2ClientId(UUID.fromString(oauth2ClientId));
}
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "OAUTH2_CLIENT", allowableValues = "OAUTH2_CLIENT")
@Override
public EntityType getEntityType() {
return EntityType.OAUTH2_CLIENT;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java
index 0261b2973e..f9f508f13f 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class OtaPackageId extends UUIDBased implements EntityId {
@Serial
@@ -37,7 +38,7 @@ public class OtaPackageId extends UUIDBased implements EntityId {
return new OtaPackageId(UUID.fromString(firmwareId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE")
@Override
public EntityType getEntityType() {
return EntityType.OTA_PACKAGE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java
index c7ee46da2a..3c00c08bc1 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class QueueId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -35,7 +36,7 @@ public class QueueId extends UUIDBased implements EntityId {
return new QueueId(UUID.fromString(queueId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "QUEUE", allowableValues = "QUEUE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "QUEUE", allowableValues = "QUEUE")
@Override
public EntityType getEntityType() {
return EntityType.QUEUE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java
index 7a921e8755..7869b7bbfe 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class QueueStatsId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -35,7 +36,7 @@ public class QueueStatsId extends UUIDBased implements EntityId {
return new QueueStatsId(UUID.fromString(queueId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "QUEUE_STATS", allowableValues = "QUEUE_STATS")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "QUEUE_STATS", allowableValues = "QUEUE_STATS")
@Override
public EntityType getEntityType() {
return EntityType.QUEUE_STATS;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java
index 559855e194..a03b0138d6 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public final class RpcId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -31,7 +32,7 @@ public final class RpcId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RPC", allowableValues = "RPC")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "RPC", allowableValues = "RPC")
@Override
public EntityType getEntityType() {
return EntityType.RPC;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java
index 0512f518af..c77cec808e 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class RuleChainId extends UUIDBased implements EntityId {
@JsonCreator
@@ -29,7 +30,7 @@ public class RuleChainId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN")
@Override
public EntityType getEntityType() {
return EntityType.RULE_CHAIN;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java
index 645acfe5e2..6be3371969 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class RuleNodeId extends UUIDBased implements EntityId {
@JsonCreator
@@ -29,7 +30,7 @@ public class RuleNodeId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RULE_NODE", allowableValues = "RULE_NODE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "RULE_NODE", allowableValues = "RULE_NODE")
@Override
public EntityType getEntityType() {
return EntityType.RULE_NODE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java
index cf171855b2..b08dcb56b4 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class TbResourceId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -31,7 +32,7 @@ public class TbResourceId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE")
@Override
public EntityType getEntityType() {
return EntityType.TB_RESOURCE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java
index f219452c07..47be35fb84 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public final class TenantId extends UUIDBased implements EntityId {
@JsonIgnore
@@ -54,7 +55,7 @@ public final class TenantId extends UUIDBased implements EntityId {
return this.equals(SYS_TENANT_ID);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TENANT", allowableValues = "TENANT")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "TENANT", allowableValues = "TENANT")
@Override
public EntityType getEntityType() {
return EntityType.TENANT;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java
index 5b46b772ba..7ced9faa43 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public class TenantProfileId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -35,7 +36,7 @@ public class TenantProfileId extends UUIDBased implements EntityId {
return new TenantProfileId(UUID.fromString(tenantProfileId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE")
@Override
public EntityType getEntityType() {
return EntityType.TENANT_PROFILE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java
index d4f113bef5..0bb939d958 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java
@@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
-@Schema
+@Schema(allOf = EntityId.class)
public class UserId extends UUIDBased implements EntityId {
@JsonCreator
@@ -34,7 +34,7 @@ public class UserId extends UUIDBased implements EntityId {
return new UserId(UUID.fromString(userId));
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "USER", allowableValues = "USER")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "USER", allowableValues = "USER")
@Override
public EntityType getEntityType() {
return EntityType.USER;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java
index fe8b40a7b9..cd9dcec8b3 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public final class WidgetTypeId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -31,7 +32,7 @@ public final class WidgetTypeId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE")
@Override
public EntityType getEntityType() {
return EntityType.WIDGET_TYPE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java
index f88d81bf74..295ac8c59d 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
+@Schema(allOf = EntityId.class)
public final class WidgetsBundleId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@@ -31,7 +32,7 @@ public final class WidgetsBundleId extends UUIDBased implements EntityId {
super(id);
}
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE")
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE")
@Override
public EntityType getEntityType() {
return EntityType.WIDGETS_BUNDLE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java
index cb4092f433..595e1aa26b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java
@@ -37,4 +37,9 @@ public interface TsKvEntry extends KvEntry, HasVersion {
return new TsValue(getTs(), getValueAsString());
}
+ @JsonIgnore
+ default boolean isDeletedEntry() {
+ return getTs() == 0 && (getValue() == null || getValueAsString().isEmpty());
+ }
+
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java
index 2211ca8bfd..bff983f2f4 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java
@@ -25,62 +25,127 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
-@Schema(description = "Default notification rule recipients configuration", allOf = NotificationRuleRecipientsConfig.class)
+@Schema(description = "Default notification rule recipients configuration")
@Data
-@EqualsAndHashCode(callSuper = true)
-public class DefaultNotificationRuleRecipientsConfig extends NotificationRuleRecipientsConfig {
+@EqualsAndHashCode
+public abstract class DefaultNotificationRuleRecipientsConfig implements NotificationRuleRecipientsConfig {
@NotEmpty
private List targets;
- @Override
- @Schema(hidden = true)
- public NotificationRuleTriggerType getTriggerType() {
- return super.getTriggerType();
- }
-
@Override
public Map> getTargetsTable() {
return Map.of(0, targets);
}
+ public static DefaultNotificationRuleRecipientsConfig forTriggerType(NotificationRuleTriggerType triggerType) {
+ return switch (triggerType) {
+ case ENTITY_ACTION -> new EntityActionRecipientsConfig();
+ case ALARM_COMMENT -> new AlarmCommentRecipientsConfig();
+ case ALARM_ASSIGNMENT -> new AlarmAssignmentRecipientsConfig();
+ case DEVICE_ACTIVITY -> new DeviceActivityRecipientsConfig();
+ case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT -> new RuleEngineComponentLifecycleEventRecipientsConfig();
+ case EDGE_CONNECTION -> new EdgeConnectionRecipientsConfig();
+ case EDGE_COMMUNICATION_FAILURE -> new EdgeCommunicationFailureRecipientsConfig();
+ case NEW_PLATFORM_VERSION -> new NewPlatformVersionRecipientsConfig();
+ case ENTITIES_LIMIT -> new EntitiesLimitRecipientsConfig();
+ case API_USAGE_LIMIT -> new ApiUsageLimitRecipientsConfig();
+ case RATE_LIMITS -> new RateLimitsRecipientsConfig();
+ case TASK_PROCESSING_FAILURE -> new TaskProcessingFailureRecipientsConfig();
+ case RESOURCES_SHORTAGE -> new ResourceShortageRecipientsConfig();
+ default -> throw new IllegalArgumentException("Unsupported trigger type for default recipients config: " + triggerType);
+ };
+ }
+
public static class EntityActionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.ENTITY_ACTION;
+ }
}
public static class AlarmCommentRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.ALARM_COMMENT;
+ }
}
public static class AlarmAssignmentRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.ALARM_ASSIGNMENT;
+ }
}
public static class DeviceActivityRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.DEVICE_ACTIVITY;
+ }
}
public static class RuleEngineComponentLifecycleEventRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT;
+ }
}
public static class EdgeConnectionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.EDGE_CONNECTION;
+ }
}
public static class EdgeCommunicationFailureRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE;
+ }
}
public static class NewPlatformVersionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.NEW_PLATFORM_VERSION;
+ }
}
public static class EntitiesLimitRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.ENTITIES_LIMIT;
+ }
}
public static class ApiUsageLimitRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.API_USAGE_LIMIT;
+ }
}
public static class RateLimitsRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.RATE_LIMITS;
+ }
}
public static class TaskProcessingFailureRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.TASK_PROCESSING_FAILURE;
+ }
}
public static class ResourceShortageRecipientsConfig extends DefaultNotificationRuleRecipientsConfig {
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.RESOURCES_SHORTAGE;
+ }
}
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java
index 37837564da..fa5a10f983 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java
@@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.EqualsAndHashCode;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.util.List;
import java.util.Map;
@@ -26,12 +27,17 @@ import java.util.UUID;
@Schema(description = "Escalated notification rule recipients configuration")
@Data
-@EqualsAndHashCode(callSuper = true)
-public class EscalatedNotificationRuleRecipientsConfig extends NotificationRuleRecipientsConfig {
+@EqualsAndHashCode
+public class EscalatedNotificationRuleRecipientsConfig implements NotificationRuleRecipientsConfig {
@NotEmpty
private Map> escalationTable;
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.ALARM;
+ }
+
@Override
public Map> getTargetsTable() {
return escalationTable;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java
index 3e44b5086e..0475a9a02d 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java
@@ -22,8 +22,6 @@ import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotNull;
-import lombok.Data;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serializable;
@@ -49,8 +47,8 @@ import java.util.UUID;
@DiscriminatorMapping(value = "TASK_PROCESSING_FAILURE", schema = DefaultNotificationRuleRecipientsConfig.TaskProcessingFailureRecipientsConfig.class),
@DiscriminatorMapping(value = "RESOURCES_SHORTAGE", schema = DefaultNotificationRuleRecipientsConfig.ResourceShortageRecipientsConfig.class)
})
-@JsonIgnoreProperties
-@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType", visible = true, include = JsonTypeInfo.As.EXISTING_PROPERTY)
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType", include = JsonTypeInfo.As.EXISTING_PROPERTY)
@JsonSubTypes({
@Type(name = "ALARM", value = EscalatedNotificationRuleRecipientsConfig.class),
@Type(name = "ENTITY_ACTION", value = DefaultNotificationRuleRecipientsConfig.EntityActionRecipientsConfig.class),
@@ -67,13 +65,11 @@ import java.util.UUID;
@Type(name = "TASK_PROCESSING_FAILURE", value = DefaultNotificationRuleRecipientsConfig.TaskProcessingFailureRecipientsConfig.class),
@Type(name = "RESOURCES_SHORTAGE", value = DefaultNotificationRuleRecipientsConfig.ResourceShortageRecipientsConfig.class)
})
-@Data
-public abstract class NotificationRuleRecipientsConfig implements Serializable {
+public interface NotificationRuleRecipientsConfig extends Serializable {
- @NotNull
- private NotificationRuleTriggerType triggerType;
+ NotificationRuleTriggerType getTriggerType();
@JsonIgnore
- public abstract Map> getTargetsTable();
+ Map> getTargetsTable();
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java
index 488d03df63..027c4a7c5c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java
@@ -15,16 +15,17 @@
*/
package org.thingsboard.server.common.data.query;
+import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import java.util.List;
import java.util.Set;
+@Schema
@Data
public class RelationsQueryFilter implements EntityFilter {
@@ -34,6 +35,7 @@ public class RelationsQueryFilter implements EntityFilter {
}
private AliasEntityId rootEntity;
+ @JsonProperty("multiRoot")
private boolean isMultiRoot;
private EntityType multiRootEntitiesType;
private Set multiRootEntityIds;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java
index 9a3834061c..2373f53283 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java
@@ -23,6 +23,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.security.DeviceCredentials;
@Schema
@@ -31,6 +32,9 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
@Data
public class DeviceExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() { return EntityType.DEVICE; }
+
@JsonProperty(index = 3)
@JsonIgnoreProperties({"id", "deviceId", "createdTime", "version"})
private DeviceCredentials credentials;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java
index 2baf5a4369..3e04074236 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java
@@ -65,6 +65,7 @@ import java.util.Map;
@Type(name = "DEVICE_PROFILE", value = EntityExportData.DeviceProfileExportData.class),
@Type(name = "ENTITY_VIEW", value = EntityExportData.EntityViewExportData.class),
@Type(name = "NOTIFICATION_TEMPLATE", value = EntityExportData.NotificationTemplateExportData.class),
+ @Type(name = "NOTIFICATION_TARGET", value = EntityExportData.NotificationTargetExportData.class),
@Type(name = "NOTIFICATION_RULE", value = EntityExportData.NotificationRuleExportData.class),
@Type(name = "AI_MODEL", value = EntityExportData.AiModelExportData.class)
})
@@ -92,7 +93,7 @@ import java.util.Map;
}
)
@Data
-public class EntityExportData> {
+public abstract class EntityExportData> {
public static final Comparator relationsComparator = Comparator
.comparing(EntityRelation::getFrom, Comparator.comparing(EntityId::getId))
@@ -111,7 +112,7 @@ public class EntityExportData> {
private E entity;
@JsonProperty(index = 1)
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
- private EntityType entityType;
+ public abstract EntityType getEntityType();
@JsonProperty(index = 100)
@ArraySchema(schema = @Schema(implementation = EntityRelation.class))
@@ -124,6 +125,28 @@ public class EntityExportData> {
@ArraySchema(schema = @Schema(implementation = CalculatedField.class))
private List calculatedFields;
+ public static EntityExportData> newInstance(EntityType entityType) {
+ return switch (entityType) {
+ case DEVICE -> new DeviceExportData();
+ case RULE_CHAIN -> new RuleChainExportData();
+ case WIDGET_TYPE -> new WidgetTypeExportData();
+ case WIDGETS_BUNDLE -> new WidgetsBundleExportData();
+ case OTA_PACKAGE -> new OtaPackageExportData();
+ case CUSTOMER -> new CustomerExportData();
+ case TB_RESOURCE -> new TbResourceExportData();
+ case DASHBOARD -> new DashboardExportData();
+ case ASSET_PROFILE -> new AssetProfileExportData();
+ case ASSET -> new AssetExportData();
+ case DEVICE_PROFILE -> new DeviceProfileExportData();
+ case ENTITY_VIEW -> new EntityViewExportData();
+ case NOTIFICATION_TEMPLATE -> new NotificationTemplateExportData();
+ case NOTIFICATION_TARGET -> new NotificationTargetExportData();
+ case NOTIFICATION_RULE -> new NotificationRuleExportData();
+ case AI_MODEL -> new AiModelExportData();
+ default -> throw new IllegalArgumentException("Unsupported entity type: " + entityType);
+ };
+ }
+
public EntityExportData sort() {
if (relations != null && !relations.isEmpty()) {
relations.sort(relationsComparator);
@@ -163,26 +186,91 @@ public class EntityExportData> {
}
@Schema
- public static class CustomerExportData extends EntityExportData {}
+ public static class CustomerExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.CUSTOMER;
+ }
+ }
+
@Schema
- public static class TbResourceExportData extends EntityExportData {}
+ public static class TbResourceExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.TB_RESOURCE;
+ }
+ }
+
@Schema
- public static class DashboardExportData extends EntityExportData {}
+ public static class DashboardExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.DASHBOARD;
+ }
+ }
+
@Schema
- public static class AssetProfileExportData extends EntityExportData {}
+ public static class AssetProfileExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.ASSET_PROFILE;
+ }
+ }
+
@Schema
- public static class AssetExportData extends EntityExportData {}
+ public static class AssetExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.ASSET;
+ }
+ }
+
@Schema
- public static class DeviceProfileExportData extends EntityExportData {}
+ public static class DeviceProfileExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.DEVICE_PROFILE;
+ }
+ }
+
@Schema
- public static class EntityViewExportData extends EntityExportData {}
+ public static class EntityViewExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.ENTITY_VIEW;
+ }
+ }
+
@Schema
- public static class NotificationTemplateExportData extends EntityExportData {}
+ public static class NotificationTemplateExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.NOTIFICATION_TEMPLATE;
+ }
+ }
+
@Schema
- public static class NotificationTargetExportData extends EntityExportData {}
+ public static class NotificationTargetExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.NOTIFICATION_TARGET;
+ }
+ }
+
@Schema
- public static class NotificationRuleExportData extends EntityExportData {}
+ public static class NotificationRuleExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.NOTIFICATION_RULE;
+ }
+ }
+
@Schema
- public static class AiModelExportData extends EntityExportData {}
+ public static class AiModelExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.AI_MODEL;
+ }
+ }
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java
index 5b10cb6fb8..727c026bc3 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java
@@ -18,12 +18,16 @@ package org.thingsboard.server.common.data.sync.ie;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage;
@Schema
@EqualsAndHashCode(callSuper = true)
public class OtaPackageExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() { return EntityType.OTA_PACKAGE; }
+
/*
* OtaPackage is not a versioned entity; its 'version' field is part of the domain model (not used for optimistic locking)
* We override both methods to ensure 'version' is not ignored during (de)serialization.
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java
index 9607415797..7b9ec8e0a5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java
@@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
@@ -30,6 +31,9 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
@Data
public class RuleChainExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() { return EntityType.RULE_CHAIN; }
+
@JsonProperty(index = 3)
@JsonIgnoreProperties({"ruleChainId", "version"})
private RuleChainMetaData metaData;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java
index b3b1130bea..f69b3603db 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.sync.ie;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
@Schema
@@ -25,4 +26,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
@EqualsAndHashCode(callSuper = true)
public class WidgetTypeExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() { return EntityType.WIDGET_TYPE; }
+
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java
index e725d20404..62140abdd1 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java
@@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import java.util.ArrayList;
@@ -32,6 +33,9 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true)
public class WidgetsBundleExportData extends EntityExportData {
+ @Override
+ public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; }
+
@ArraySchema(arraySchema = @Schema(description = "List of widgets in the bundle"), schema = @Schema(implementation = JsonNode.class))
@JsonProperty(index = 3)
private List widgets;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java
index 398f4b6929..1cbaa957f0 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java
@@ -33,7 +33,7 @@ import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
-@JsonPropertyOrder({"fqn", "name", "deprecated", "image", "description", "descriptor", "externalId", "resources"})
+@JsonPropertyOrder({"id", "createdTime", "tenantId", "fqn", "name", "deprecated", "scada", "version", "descriptor", "image", "description", "tags", "externalId", "resources"})
public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity {
@Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.")
diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java
index d645d40dcc..9d6434a7c2 100644
--- a/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java
+++ b/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java
@@ -15,8 +15,18 @@
*/
package org.thingsboard.server.common.data.id;
+import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
public class EntityIdTest {
@@ -25,4 +35,44 @@ public class EntityIdTest {
Assertions.assertEquals("13814000-1dd2-11b2-8080-808080808080", EntityId.NULL_UUID.toString());
}
+ @Test
+ public void allEntityIdImplementors_shouldBeInDiscriminatorMapping() {
+ Schema schemaAnnotation = EntityId.class.getAnnotation(Schema.class);
+ assertThat(schemaAnnotation).as("EntityId must have @Schema annotation").isNotNull();
+
+ DiscriminatorMapping[] mappings = schemaAnnotation.discriminatorMapping();
+ Map> discriminatorMap = Arrays.stream(mappings)
+ .collect(Collectors.toMap(DiscriminatorMapping::value, DiscriminatorMapping::schema));
+
+ UUID testUuid = UUID.randomUUID();
+ for (EntityType entityType : EntityType.values()) {
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, testUuid);
+ String typeName = entityType.name();
+
+ assertThat(discriminatorMap)
+ .as("EntityId @Schema discriminatorMapping is missing entry for EntityType." + typeName)
+ .containsKey(typeName);
+ assertThat(discriminatorMap.get(typeName))
+ .as("Discriminator mapping for " + typeName + " should point to " + entityId.getClass().getSimpleName())
+ .isEqualTo(entityId.getClass());
+ }
+ }
+
+ @Test
+ public void allEntityIdImplementors_shouldHaveAllOfEntityId() {
+ UUID testUuid = UUID.randomUUID();
+ for (EntityType entityType : EntityType.values()) {
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, testUuid);
+ Class> idClass = entityId.getClass();
+ Schema schemaAnnotation = idClass.getAnnotation(Schema.class);
+
+ assertThat(schemaAnnotation)
+ .as(idClass.getSimpleName() + " must have @Schema annotation")
+ .isNotNull();
+ assertThat(schemaAnnotation.allOf())
+ .as(idClass.getSimpleName() + " @Schema must include allOf = EntityId.class")
+ .contains(EntityId.class);
+ }
+ }
+
}
\ No newline at end of file
diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/sync/ie/EntityExportDataTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/sync/ie/EntityExportDataTest.java
new file mode 100644
index 0000000000..dd932fa2c5
--- /dev/null
+++ b/common/data/src/test/java/org/thingsboard/server/common/data/sync/ie/EntityExportDataTest.java
@@ -0,0 +1,52 @@
+/**
+ * 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.common.data.sync.ie;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import org.junit.jupiter.api.Test;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class EntityExportDataTest {
+
+ @Test
+ public void newInstance_shouldSupportAllJsonSubTypes() {
+ JsonSubTypes subTypes = EntityExportData.class.getAnnotation(JsonSubTypes.class);
+ assertThat(subTypes).as("EntityExportData must have @JsonSubTypes annotation").isNotNull();
+
+ Set jsonSubTypeNames = Arrays.stream(subTypes.value())
+ .map(JsonSubTypes.Type::name)
+ .collect(Collectors.toSet());
+
+ for (String typeName : jsonSubTypeNames) {
+ EntityType entityType = EntityType.valueOf(typeName);
+ EntityExportData> instance = EntityExportData.newInstance(entityType);
+
+ assertThat(instance)
+ .as("newInstance(%s) should not return null", typeName)
+ .isNotNull();
+ assertThat(instance.getEntityType())
+ .as("newInstance(%s).getEntityType() should return %s", typeName, entityType)
+ .isEqualTo(entityType);
+ }
+ }
+
+}
diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto
index 472ce8e8dd..48ec26f875 100644
--- a/common/edge-api/src/main/proto/edge.proto
+++ b/common/edge-api/src/main/proto/edge.proto
@@ -47,8 +47,10 @@ enum EdgeVersion {
V_4_3_0 = 13;
V_4_2_1_2 = 14;
V_4_2_2 = 4220;
+ V_4_2_2_1 = 4221;
V_4_3_0_1 = 15;
V_4_3_1 = 4310;
+ V_4_3_1_1 = 4311;
V_4_4_0 = 4400;
V_LATEST = 99999;
@@ -146,6 +148,13 @@ message AiModelUpdateMsg{
string entity = 4;
}
+message ApiKeyUpdateMsg{
+ UpdateMsgType msgType = 1;
+ int64 idMSB = 2;
+ int64 idLSB = 3;
+ string entity = 4;
+}
+
message EntityDataProto {
int64 entityIdMSB = 1;
int64 entityIdLSB = 2;
@@ -456,6 +465,7 @@ message UplinkMsg {
repeated AiModelUpdateMsg aiModelUpdateMsg = 27;
repeated UserUpdateMsg userUpdateMsg = 28;
repeated UserCredentialsUpdateMsg userCredentialsUpdateMsg = 29;
+ repeated ApiKeyUpdateMsg apiKeyUpdateMsg = 30;
}
message UplinkResponseMsg {
@@ -507,4 +517,5 @@ message DownlinkMsg {
repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34;
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35;
repeated AiModelUpdateMsg aiModelUpdateMsg = 36;
+ repeated ApiKeyUpdateMsg apiKeyUpdateMsg = 37;
}
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java
index 3d05e999e0..10245c5a24 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java
@@ -88,20 +88,30 @@ public class MqttTransportContext extends TransportContext {
@Value("${transport.mqtt.proxy_enabled:false}")
private boolean proxyEnabled;
- private final AtomicInteger connectionsCounter = new AtomicInteger();
+ private final AtomicInteger connectionsActiveCounterMQTT = new AtomicInteger();
+ private final AtomicInteger connectionsActiveCounterMQTTS = new AtomicInteger();
@PostConstruct
public void init() {
super.init();
- transportService.createGaugeStats("openConnections", connectionsCounter);
+ transportService.createGaugeStats("connections_active", connectionsActiveCounterMQTT, "protocol", "MQTT");
+ transportService.createGaugeStats("connections_active", connectionsActiveCounterMQTTS, "protocol", "MQTTS");
}
- public void channelRegistered() {
- connectionsCounter.incrementAndGet();
+ public void channelRegistered(boolean isSSL) {
+ if (isSSL) {
+ connectionsActiveCounterMQTTS.incrementAndGet();
+ } else {
+ connectionsActiveCounterMQTT.incrementAndGet();
+ }
}
- public void channelUnregistered() {
- connectionsCounter.decrementAndGet();
+ public void channelUnregistered(boolean isSSL) {
+ if (isSSL) {
+ connectionsActiveCounterMQTTS.decrementAndGet();
+ } else {
+ connectionsActiveCounterMQTT.decrementAndGet();
+ }
}
public boolean checkAddress(InetSocketAddress address) {
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index f753579292..48fbad3032 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -180,16 +180,20 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
this.rpcAwaitingAck = new ConcurrentHashMap<>();
}
+ boolean isSSL() {
+ return sslHandler != null;
+ }
+
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
super.channelRegistered(ctx);
- context.channelRegistered();
+ context.channelRegistered(isSSL());
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx);
- context.channelUnregistered();
+ context.channelUnregistered(isSSL());
}
@Override
@@ -254,6 +258,17 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
}
+ String getClientAddr(ChannelHandlerContext ctx) {
+ try {
+ InetSocketAddress remote = getAddress(ctx);
+ if (remote == null) return "unknown";
+ String host = remote.getAddress() != null ? remote.getAddress().getHostAddress() : remote.getHostString();
+ return host + ":" + remote.getPort();
+ } catch (Exception ignored) {
+ return "unknown";
+ }
+ }
+
InetSocketAddress getAddress(ChannelHandlerContext ctx) {
var address = ctx.channel().attr(MqttTransportService.ADDRESS).get();
if (address == null) {
@@ -1150,19 +1165,25 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (cause instanceof IOException) {
if (log.isDebugEnabled()) {
- log.debug("[{}][{}][{}] IOException: {}", sessionId,
+ String clientAddr = getClientAddr(ctx);
+ log.debug("[{}][{}][{}][{}] {}: {}", sessionId,
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null),
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""),
+ clientAddr,
+ cause.getClass().getSimpleName(),
cause.getMessage(),
cause);
} else if (log.isInfoEnabled()) {
- log.info("[{}][{}][{}] IOException: {}", sessionId,
+ String clientAddr = getClientAddr(ctx);
+ log.info("[{}][{}][{}][{}] {}: {}", sessionId,
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null),
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""),
+ clientAddr,
+ cause.getClass().getSimpleName(),
cause.getMessage());
}
} else {
- log.error("[{}] Unexpected Exception", sessionId, cause);
+ log.error("[{}][{}] Unexpected Exception", sessionId, getClientAddr(ctx), cause);
}
closeCtx(ctx, MqttReasonCodes.Disconnect.SERVER_SHUTTING_DOWN);
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
index 5c7d552855..a8f8a8b863 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
@@ -161,5 +161,5 @@ public interface TransportService {
boolean hasSession(SessionInfoProto sessionInfo);
- void createGaugeStats(String openConnections, AtomicInteger connectionsCounter);
+ void createGaugeStats(String statsName, AtomicInteger number, String... tags);
}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
index 260957c990..e8ade498db 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
@@ -1257,9 +1257,21 @@ public class DefaultTransportService extends TransportActivityManager implements
}
@Override
- public void createGaugeStats(String statsName, AtomicInteger number) {
- statsFactory.createGauge(StatsType.TRANSPORT + "." + statsName, number);
- statsMap.put(statsName, number);
+ public void createGaugeStats(String statsName, AtomicInteger number, String... tags) {
+ String key = "thingsboard" + "." + StatsType.TRANSPORT.getName() + "." + statsName;
+ statsFactory.createGauge(key, number, tags);
+ statsMap.put(statsName + tagsKey(tags), number);
+ }
+
+ private String tagsKey(String... tags) {
+ if (tags == null || tags.length < 2) return "";
+ StringBuilder sb = new StringBuilder("[");
+ for (int i = 0; i < tags.length; i += 2) {
+ if (i > 0) sb.append(',');
+ sb.append(tags[i]).append('=').append(i + 1 < tags.length ? tags[i + 1] : "");
+ }
+ sb.append(']');
+ return sb.toString();
}
@Scheduled(fixedDelayString = "${transport.stats.print-interval-ms:60000}")
diff --git a/common/util/src/main/java/org/thingsboard/common/util/SsrfProtectionValidator.java b/common/util/src/main/java/org/thingsboard/common/util/SsrfProtectionValidator.java
index 15da77f663..9132d117ae 100644
--- a/common/util/src/main/java/org/thingsboard/common/util/SsrfProtectionValidator.java
+++ b/common/util/src/main/java/org/thingsboard/common/util/SsrfProtectionValidator.java
@@ -38,6 +38,7 @@ public class SsrfProtectionValidator {
private static final Set BLOCKED_HOSTNAME_SUFFIXES = Set.of(".internal", ".local");
private static volatile AdditionalBlockedHosts additionalBlocked = AdditionalBlockedHosts.EMPTY;
+ private static volatile AllowedHosts allowedHosts = AllowedHosts.EMPTY;
// Well-known cloud metadata endpoints not covered by the JDK checks (isLoopback, isSiteLocal, isLinkLocal)
private static final List CLOUD_METADATA_RANGES = List.of(
@@ -66,6 +67,13 @@ public class SsrfProtectionValidator {
}
String hostLower = host.toLowerCase();
+
+ // Allow-listed hostnames bypass all hostname and IP checks
+ AllowedHosts currentAllowed = allowedHosts;
+ if (currentAllowed.hostnames.contains(hostLower)) {
+ return;
+ }
+
if (BLOCKED_HOSTNAMES.contains(hostLower) || additionalBlocked.hostnames.contains(hostLower)) {
throwBlockedHost(host);
}
@@ -98,7 +106,15 @@ public class SsrfProtectionValidator {
}
}
- private static boolean isBlockedAddress(InetAddress address) {
+ public static boolean isBlockedAddress(InetAddress address) {
+ // Check allow-list first: allowed addresses bypass all block checks
+ AllowedHosts currentAllowed = allowedHosts;
+ for (CidrRange cidr : currentAllowed.cidrRanges) {
+ if (cidr.contains(address)) {
+ return false;
+ }
+ }
+
// Covers 127.0.0.0/8 and ::1
if (address.isLoopbackAddress()) {
return true;
@@ -142,14 +158,37 @@ public class SsrfProtectionValidator {
throw new RuntimeException("URI is invalid: host '" + host + "' is not allowed");
}
+ public static boolean isEnabled() {
+ return enabled;
+ }
+
public static void setEnabled(boolean enabled) {
SsrfProtectionValidator.enabled = enabled;
}
public static void setAdditionalBlockedHosts(List entries) {
+ ParsedHostEntries parsed = parseHostEntries(entries);
+ additionalBlocked = new AdditionalBlockedHosts(parsed.cidrRanges, parsed.hostnames);
+ if (!parsed.cidrRanges.isEmpty() || !parsed.hostnames.isEmpty()) {
+ log.info("SSRF additional blocked hosts configured: {} CIDR range(s), {} hostname(s)", parsed.cidrRanges.size(), parsed.hostnames.size());
+ }
+ }
+
+ public static void setAllowedHosts(List entries) {
+ ParsedHostEntries parsed = parseHostEntries(entries);
+ allowedHosts = new AllowedHosts(parsed.cidrRanges, parsed.hostnames);
+ if (!parsed.cidrRanges.isEmpty() || !parsed.hostnames.isEmpty()) {
+ log.info("SSRF allowed hosts configured: {} CIDR range(s), {} hostname(s)", parsed.cidrRanges.size(), parsed.hostnames.size());
+ }
+ }
+
+ public static boolean isHostnameAllowed(String hostname) {
+ return allowedHosts.hostnames.contains(hostname.toLowerCase());
+ }
+
+ private static ParsedHostEntries parseHostEntries(List entries) {
if (entries == null || entries.isEmpty()) {
- additionalBlocked = AdditionalBlockedHosts.EMPTY;
- return;
+ return ParsedHostEntries.EMPTY;
}
List cidrRanges = new ArrayList<>();
Set hostnames = new HashSet<>();
@@ -168,10 +207,9 @@ public class SsrfProtectionValidator {
hostnames.add(trimmed.toLowerCase());
}
}
- additionalBlocked = new AdditionalBlockedHosts(
+ return new ParsedHostEntries(
Collections.unmodifiableList(cidrRanges),
Collections.unmodifiableSet(hostnames));
- log.info("SSRF additional blocked hosts configured: {} CIDR range(s), {} hostname(s)", cidrRanges.size(), hostnames.size());
}
private static boolean isIpLiteral(String entry) {
@@ -179,10 +217,18 @@ public class SsrfProtectionValidator {
return !entry.isEmpty() && (Character.isDigit(entry.charAt(0)) || entry.contains(":"));
}
+ private record ParsedHostEntries(List cidrRanges, Set hostnames) {
+ static final ParsedHostEntries EMPTY = new ParsedHostEntries(Collections.emptyList(), Collections.emptySet());
+ }
+
record AdditionalBlockedHosts(List cidrRanges, Set hostnames) {
static final AdditionalBlockedHosts EMPTY = new AdditionalBlockedHosts(Collections.emptyList(), Collections.emptySet());
}
+ record AllowedHosts(List cidrRanges, Set hostnames) {
+ static final AllowedHosts EMPTY = new AllowedHosts(Collections.emptyList(), Collections.emptySet());
+ }
+
record CidrRange(byte[] network, int prefixLength) {
static CidrRange of(String ip, int prefixLength) {
diff --git a/common/util/src/test/java/org/thingsboard/common/util/SsrfProtectionValidatorTest.java b/common/util/src/test/java/org/thingsboard/common/util/SsrfProtectionValidatorTest.java
index 6cb2d21a9a..d2d1be80c9 100644
--- a/common/util/src/test/java/org/thingsboard/common/util/SsrfProtectionValidatorTest.java
+++ b/common/util/src/test/java/org/thingsboard/common/util/SsrfProtectionValidatorTest.java
@@ -20,10 +20,12 @@ import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
+import java.net.InetAddress;
import java.net.URI;
import java.util.Collections;
import java.util.List;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -335,4 +337,244 @@ public class SsrfProtectionValidatorTest {
}
}
+ // --- Allow-list tests ---
+
+ @Test
+ void testAllowListCidrAllowsPrivateAddress() {
+ try {
+ SsrfProtectionValidator.setAllowedHosts(List.of("192.168.1.0/24"));
+ // 192.168.1.1 is normally blocked (site-local), but allow-listed
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.1"), true));
+ // Other private ranges remain blocked
+ assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://10.0.0.1"), true))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("URI is invalid");
+ } finally {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+ }
+
+ @Test
+ void testAllowListHostnameBypassesSuffixCheck() {
+ try {
+ SsrfProtectionValidator.setAllowedHosts(List.of("my-device.local"));
+ // .local suffix is normally blocked, but allow-listed hostname passes
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://my-device.local/api"), true));
+ // Other .local hostnames remain blocked
+ assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://other-device.local/api"), true))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("URI is invalid");
+ } finally {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+ }
+
+ @Test
+ void testAllowListPrecedenceOverBlockList() {
+ try {
+ // Block 8.8.8.0/24 via additional-blocked, but allow 8.8.8.8 via allow-list
+ SsrfProtectionValidator.setAdditionalBlockedHosts(List.of("8.8.8.0/24"));
+ SsrfProtectionValidator.setAllowedHosts(List.of("8.8.8.8"));
+ // Allow-list should win
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("https://8.8.8.8"), true));
+ // Adjacent IP still blocked
+ assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("https://8.8.8.9"), true))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("URI is invalid");
+ } finally {
+ SsrfProtectionValidator.setAdditionalBlockedHosts(Collections.emptyList());
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+ }
+
+ @Test
+ void testIsBlockedAddressPublicApi() throws Exception {
+ InetAddress loopback = InetAddress.getByName("127.0.0.1");
+ assertThat(SsrfProtectionValidator.isBlockedAddress(loopback)).isTrue();
+
+ InetAddress publicIp = InetAddress.getByName("8.8.8.8");
+ assertThat(SsrfProtectionValidator.isBlockedAddress(publicIp)).isFalse();
+
+ // Allow-listed private address
+ try {
+ SsrfProtectionValidator.setAllowedHosts(List.of("10.0.0.0/8"));
+ InetAddress privateIp = InetAddress.getByName("10.1.2.3");
+ assertThat(SsrfProtectionValidator.isBlockedAddress(privateIp)).isFalse();
+ } finally {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+ }
+
+ @Test
+ void testIsEnabledAccessor() {
+ boolean original = SsrfProtectionValidator.isEnabled();
+ try {
+ SsrfProtectionValidator.setEnabled(true);
+ assertThat(SsrfProtectionValidator.isEnabled()).isTrue();
+ SsrfProtectionValidator.setEnabled(false);
+ assertThat(SsrfProtectionValidator.isEnabled()).isFalse();
+ } finally {
+ SsrfProtectionValidator.setEnabled(original);
+ }
+ }
+
+ @Test
+ void testSetAllowedHostsEmptyAndNull() {
+ // Should not throw
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ SsrfProtectionValidator.setAllowedHosts(null);
+ }
+
+ @Test
+ void testIsHostnameAllowed() {
+ try {
+ SsrfProtectionValidator.setAllowedHosts(List.of("my-device.local", "Internal-Server.Corp"));
+ assertThat(SsrfProtectionValidator.isHostnameAllowed("my-device.local")).isTrue();
+ assertThat(SsrfProtectionValidator.isHostnameAllowed("MY-DEVICE.LOCAL")).isTrue(); // case-insensitive
+ assertThat(SsrfProtectionValidator.isHostnameAllowed("internal-server.corp")).isTrue();
+ assertThat(SsrfProtectionValidator.isHostnameAllowed("other-device.local")).isFalse();
+ assertThat(SsrfProtectionValidator.isHostnameAllowed("example.com")).isFalse();
+ } finally {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+ }
+
+ @Test
+ void testIsHostnameAllowedEmptyList() {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ assertThat(SsrfProtectionValidator.isHostnameAllowed("anything")).isFalse();
+ }
+
+ @Test
+ void testValidateUriUsesStaticEnabledFlag() {
+ boolean original = SsrfProtectionValidator.isEnabled();
+ try {
+ // When enabled, loopback is blocked via the public one-arg overload
+ SsrfProtectionValidator.setEnabled(true);
+ assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://127.0.0.1")))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("URI is invalid");
+
+ // When disabled, loopback passes
+ SsrfProtectionValidator.setEnabled(false);
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://127.0.0.1")));
+ } finally {
+ SsrfProtectionValidator.setEnabled(original);
+ }
+ }
+
+ @Test
+ void testAllowListHostnameCaseInsensitive() {
+ try {
+ SsrfProtectionValidator.setAllowedHosts(List.of("My-Device.LOCAL"));
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://my-device.local/api"), true));
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://MY-DEVICE.LOCAL/api"), true));
+ } finally {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+ }
+
+ @Test
+ void testAllowListOverridesCloudMetadataRange() {
+ try {
+ // 169.254.169.254 is link-local (blocked by default), allow-list should override
+ SsrfProtectionValidator.setAllowedHosts(List.of("169.254.169.254"));
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://169.254.169.254/latest/meta-data/"), true));
+ // Other link-local still blocked
+ assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://169.254.1.1"), true))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("URI is invalid");
+ } finally {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+ }
+
+ @Test
+ void testAllowListOverridesLoopback() {
+ try {
+ SsrfProtectionValidator.setAllowedHosts(List.of("127.0.0.0/8"));
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://127.0.0.1"), true));
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://127.1.2.3"), true));
+ } finally {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+ }
+
+ @Test
+ void testAllowListCidrBoundary() {
+ try {
+ SsrfProtectionValidator.setAllowedHosts(List.of("192.168.1.0/24"));
+ // Last address in range
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.255"), true));
+ // First address outside range
+ assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.2.0"), true))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("URI is invalid");
+ // Different subnet entirely
+ assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.0.1"), true))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("URI is invalid");
+ } finally {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+ }
+
+ @Test
+ void testBlockedIpv6UniqueLocal() throws Exception {
+ // fc00::/7 covers fc00:: through fdff::
+ InetAddress fc00 = InetAddress.getByName("fc00::1");
+ assertThat(SsrfProtectionValidator.isBlockedAddress(fc00)).isTrue();
+
+ InetAddress fdAddr = InetAddress.getByName("fd12:3456:789a::1");
+ assertThat(SsrfProtectionValidator.isBlockedAddress(fdAddr)).isTrue();
+
+ // fe00:: is NOT in fc00::/7 (it's in fe80::/10 link-local, but fe00:: without the 80 bits is different)
+ // 2001:db8:: is a public documentation prefix, not blocked
+ InetAddress publicV6 = InetAddress.getByName("2001:db8::1");
+ assertThat(SsrfProtectionValidator.isBlockedAddress(publicV6)).isFalse();
+ }
+
+ @Test
+ void testParseHostEntriesWithWhitespaceAndBlanks() {
+ try {
+ SsrfProtectionValidator.setAllowedHosts(List.of(" 192.168.1.0/24 ", "", " ", "my-host.corp"));
+ // Trimmed CIDR works
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.1"), true));
+ // Trimmed hostname works
+ assertThat(SsrfProtectionValidator.isHostnameAllowed("my-host.corp")).isTrue();
+ } finally {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+ }
+
+ @Test
+ void testSetAllowedHostsReplacePrevious() {
+ try {
+ SsrfProtectionValidator.setAllowedHosts(List.of("192.168.1.0/24"));
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.1"), true));
+
+ // Replace with different range
+ SsrfProtectionValidator.setAllowedHosts(List.of("10.0.0.0/8"));
+ // Old range no longer allowed
+ assertThatThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://192.168.1.1"), true))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("URI is invalid");
+ // New range allowed
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://10.1.2.3"), true));
+ } finally {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+ }
+
+ @Test
+ void testAllowListHostnameBypassesBlockedHostname() {
+ try {
+ // "localhost" is in BLOCKED_HOSTNAMES; allow-listing it should let it through
+ SsrfProtectionValidator.setAllowedHosts(List.of("localhost"));
+ assertThatNoException().isThrownBy(() -> SsrfProtectionValidator.validateUri(URI.create("http://localhost/path"), true));
+ } finally {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+ }
+
}
diff --git a/dao/pom.xml b/dao/pom.xml
index 928a107079..d526eeed4f 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -280,7 +280,6 @@
org.apache.maven.plugins
maven-jar-plugin
- ${jar-plugin.version}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
index a19ba6c0e7..4b469851cd 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao;
+import org.hibernate.exception.ConstraintViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -202,4 +203,13 @@ public final class DaoUtil {
.collect(Collectors.toList());
}
+ public static ConstraintViolationException extractConstraintViolation(Throwable t) {
+ if (t instanceof ConstraintViolationException cve) {
+ return cve;
+ } else if (t != null && t.getCause() instanceof ConstraintViolationException cve) {
+ return cve;
+ }
+ return null;
+ }
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index 9f9e4a67e9..69a04f301b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -730,6 +730,7 @@ public class ModelConstants {
public static final String CALCULATED_FIELD_CONFIGURATION_VERSION = "configuration_version";
public static final String CALCULATED_FIELD_CONFIGURATION = "configuration";
public static final String CALCULATED_FIELD_VERSION = "version";
+ public static final String CALCULATED_FIELD_ADDITIONAL_INFO = ADDITIONAL_INFO_PROPERTY;
/**
* Tasks constants.
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java
index 5a507b7fd3..47c1520ac0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java
@@ -36,6 +36,7 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter;
import java.util.UUID;
+import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ADDITIONAL_INFO;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_ID;
@@ -81,9 +82,11 @@ public class CalculatedFieldEntity extends BaseVersionedEntity
@Column(name = DEBUG_SETTINGS)
private String debugSettings;
- public CalculatedFieldEntity() {
- super();
- }
+ @Convert(converter = JsonConverter.class)
+ @Column(name = CALCULATED_FIELD_ADDITIONAL_INFO)
+ private JsonNode additionalInfo;
+
+ public CalculatedFieldEntity() {}
public CalculatedFieldEntity(CalculatedField calculatedField) {
this.setUuid(calculatedField.getUuidId());
@@ -97,6 +100,7 @@ public class CalculatedFieldEntity extends BaseVersionedEntity
this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration());
this.version = calculatedField.getVersion();
this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings());
+ this.additionalInfo = calculatedField.getAdditionalInfo();
}
@Override
@@ -111,6 +115,7 @@ public class CalculatedFieldEntity extends BaseVersionedEntity
calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class));
calculatedField.setVersion(version);
calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
+ calculatedField.setAdditionalInfo(additionalInfo);
return calculatedField;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
index b46540aa55..fb8d5eb1d8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
@@ -511,12 +511,10 @@ public class DefaultNotifications {
rule.setTriggerConfig(defaultRule.getTriggerConfig());
if (rule.getTriggerType() == NotificationRuleTriggerType.ALARM) {
EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig();
- recipientsConfig.setTriggerType(rule.getTriggerType());
recipientsConfig.setEscalationTable(Map.of(0, toUUIDs(List.of(targets))));
rule.setRecipientsConfig(recipientsConfig);
} else {
- DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig();
- recipientsConfig.setTriggerType(rule.getTriggerType());
+ DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(rule.getTriggerType());
recipientsConfig.setTargets(toUUIDs(List.of(targets)));
rule.setRecipientsConfig(recipientsConfig);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationRequestDao.java b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationRequestDao.java
index 96a86073d4..8e1408f4fc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationRequestDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationRequestDao.java
@@ -50,7 +50,7 @@ public interface NotificationRequestDao extends Dao {
boolean existsByTenantIdAndStatusAndTemplateId(TenantId tenantId, NotificationRequestStatus status, NotificationTemplateId templateId);
- int removeAllByCreatedTimeBefore(long ts);
+ int removeByTenantIdAndCreatedTimeBeforeBatch(TenantId tenantId, long ts, int batchSize);
NotificationRequestInfo findInfoById(TenantId tenantId, NotificationRequestId id);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java
index 484b3c49b3..2e30c0d598 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java
@@ -196,7 +196,9 @@ public class BaseOtaPackageService extends AbstractCachedEntityService INCORRECT_OTA_PACKAGE_ID + id);
try {
+ Long oid = getDataOidById(tenantId, otaPackageId);
otaPackageDao.removeById(tenantId, otaPackageId.getId());
+ unlinkDataIfPresent(tenantId, otaPackageId, oid);
publishEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId));
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(otaPackageId).build());
} catch (Exception t) {
@@ -215,6 +217,30 @@ public class BaseOtaPackageService extends AbstractCachedEntityService tenantOtaPackageRemover =
- new PaginatedRemover<>() {
-
- @Override
- protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
- return otaPackageInfoDao.findOtaPackageInfoByTenantId(id, pageLink);
- }
+ private final PaginatedRemover tenantOtaPackageRemover = new PaginatedRemover<>() {
+ @Override
+ protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
+ return otaPackageInfoDao.findOtaPackageInfoByTenantId(id, pageLink);
+ }
- @Override
- protected void removeEntity(TenantId tenantId, OtaPackageInfo entity) {
- deleteOtaPackage(tenantId, entity.getId());
- }
- };
+ @Override
+ protected void removeEntity(TenantId tenantId, OtaPackageInfo entity) {
+ deleteOtaPackage(tenantId, entity.getId());
+ }
+ };
@Override
public Optional> findEntity(TenantId tenantId, EntityId entityId) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java
index c11a13cbe1..875aaea4ed 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java
@@ -18,15 +18,20 @@ package org.thingsboard.server.dao.ota;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.TenantEntityWithDataDao;
+import java.util.UUID;
+
public interface OtaPackageDao extends Dao, TenantEntityWithDataDao, ExportableEntityDao {
Long sumDataSizeByTenantId(TenantId tenantId);
OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version);
+ Long getDataOidById(UUID id);
+
+ Integer unlinkLargeObject(Long dataOid);
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java
index d71d6502cf..f5ff79fb52 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java
@@ -17,15 +17,22 @@ package org.thingsboard.server.dao.pat;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.dao.Dao;
+import java.util.List;
import java.util.Set;
public interface ApiKeyDao extends Dao {
ApiKey findByValue(String value);
+ PageData findByTenantId(TenantId tenantId, PageLink pageLink);
+
+ List findByTenantIdAndUserId(TenantId tenantId, UserId userId);
+
Set deleteByTenantId(TenantId tenantId);
Set deleteByUserId(TenantId tenantId, UserId userId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java
index 17a3e2bda3..36d8736f78 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java
@@ -24,6 +24,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
@@ -34,9 +35,11 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.pat.ApiKeyInfo;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
+import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.service.validator.ApiKeyDataValidator;
+import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@@ -73,13 +76,20 @@ public class ApiKeyServiceImpl extends AbstractCachedEntityService findApiKeysByUserId(TenantId tenantId, UserId userId) {
+ log.trace("Executing findApiKeysByUserId [{}][{}]", tenantId, userId);
+ validateId(userId, id -> INCORRECT_USER_ID + id);
+ return apiKeyDao.findByTenantIdAndUserId(tenantId, userId);
+ }
+
@Override
public Optional> findEntity(TenantId tenantId, EntityId entityId) {
return Optional.ofNullable(findApiKeyById(tenantId, new ApiKeyId(entityId.getId())));
@@ -126,6 +143,20 @@ public class ApiKeyServiceImpl extends AbstractCachedEntityService INCORRECT_API_KEY_ID + id);
apiKeyDao.removeById(tenantId, apiKeyId);
publishEvictEvent(new ApiKeyEvictEvent(apiKey.getValue()));
+ eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(apiKey.getId()).build());
+ }
+
+ @Override
+ public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
+ ApiKey apiKey = findApiKeyById(tenantId, new ApiKeyId(id.getId()));
+ if (apiKey == null) {
+ if (force) {
+ return;
+ } else {
+ throw new DataValidationException("Unable to delete non-existent API key.");
+ }
+ }
+ deleteApiKey(tenantId, apiKey, force);
}
@Override
@@ -144,6 +175,13 @@ public class ApiKeyServiceImpl extends AbstractCachedEntityService publishEvictEvent(new ApiKeyEvictEvent(value)));
}
+ @Override
+ public PageData findApiKeysByTenantId(TenantId tenantId, PageLink pageLink) {
+ log.trace("Executing findApiKeysByTenantId [{}]", tenantId);
+ validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
+ return apiKeyDao.findByTenantId(tenantId, pageLink);
+ }
+
@Override
public ApiKey findApiKeyByValue(String value) {
log.trace("Executing findApiKeyByValue [{}]", value);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java b/dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java
index f88b672a34..37fe2950b4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java
@@ -24,12 +24,13 @@ import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.dao.Dao;
public interface RpcDao extends Dao {
+
PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink);
PageData findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink);
PageData findAllRpcByTenantId(TenantId tenantId, PageLink pageLink);
- int deleteOutdatedRpcByTenantId(TenantId tenantId, Long expirationTime);
+ int deleteOutdatedRpcByTenantIdBatch(TenantId tenantId, Long expirationTime, int batchSize);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java
index 42e872a87c..289220710c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service.validator;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
+import org.thingsboard.common.util.SsrfProtectionValidator;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.MapperType;
@@ -28,6 +29,8 @@ import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
+import java.net.URI;
+
@Component
@AllArgsConstructor
public class Oauth2ClientDataValidator extends DataValidator {
@@ -64,6 +67,11 @@ public class Oauth2ClientDataValidator extends DataValidator {
if (StringUtils.isEmpty(customConfig.getUrl())) {
throw new DataValidationException("Custom mapper URL should be specified!");
}
+ try {
+ SsrfProtectionValidator.validateUri(new URI(customConfig.getUrl()));
+ } catch (Exception e) {
+ throw new DataValidationException("Custom mapper URL is not allowed: " + e.getMessage());
+ }
}
}
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java
index f6113155ca..0f9dcc20be 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java
@@ -74,6 +74,7 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
JsonNode configuration = JacksonUtil.toJsonNode((String) row.get("configuration"));
long version = row.get("version") != null ? (long) row.get("version") : 0;
String debugSettings = (String) row.get("debug_settings");
+ JsonNode additionalInfo = JacksonUtil.toJsonNode((String) row.get("additional_info"));
CalculatedField calculatedField = new CalculatedField();
calculatedField.setId(new CalculatedFieldId(id));
@@ -91,6 +92,7 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
}
calculatedField.setVersion(version);
calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
+ calculatedField.setAdditionalInfo(additionalInfo);
return calculatedField;
}).collect(Collectors.toList());
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRequestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRequestDao.java
index 9d32e91ca2..f4cc406a75 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRequestDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRequestDao.java
@@ -98,8 +98,8 @@ public class JpaNotificationRequestDao extends JpaAbstractDao findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable);
+ // The 'data' column is of type OID (PostgreSQL large object reference), so it returns the OID as Long
+ @Query(value = "SELECT data FROM ota_package WHERE id = :id AND data IS NOT NULL", nativeQuery = true)
+ Long getDataOidById(@Param("id") UUID id);
+
+ @Transactional
+ @Query(value = "SELECT lo_unlink(:oid)", nativeQuery = true)
+ Integer unlinkLargeObject(@Param("oid") Long oid);
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java
index e3e560725c..a83223d726 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java
@@ -15,6 +15,8 @@
*/
package org.thingsboard.server.dao.sql.pat;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
@@ -22,6 +24,7 @@ import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sql.ApiKeyEntity;
+import java.util.List;
import java.util.Set;
import java.util.UUID;
@@ -29,6 +32,10 @@ public interface ApiKeyRepository extends JpaRepository {
ApiKeyEntity findByValue(String value);
+ Page findByTenantId(UUID tenantId, Pageable pageable);
+
+ List findByTenantIdAndUserId(UUID tenantId, UUID userId);
+
@Transactional
@Modifying
@Query(value = """
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java
index 61ca7bd142..69de36384e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java
@@ -22,6 +22,8 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.ApiKeyEntity;
@@ -29,6 +31,7 @@ import org.thingsboard.server.dao.pat.ApiKeyDao;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
+import java.util.List;
import java.util.Set;
import java.util.UUID;
@@ -45,6 +48,16 @@ public class JpaApiKeyDao extends JpaAbstractDao implement
return DaoUtil.getData(apiKeyRepository.findByValue(value));
}
+ @Override
+ public PageData findByTenantId(TenantId tenantId, PageLink pageLink) {
+ return DaoUtil.toPageData(apiKeyRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)));
+ }
+
+ @Override
+ public List findByTenantIdAndUserId(TenantId tenantId, UserId userId) {
+ return DaoUtil.convertDataList(apiKeyRepository.findByTenantIdAndUserId(tenantId.getId(), userId.getId()));
+ }
+
@Override
public Set deleteByTenantId(TenantId tenantId) {
return apiKeyRepository.deleteByTenantId(tenantId.getId());
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java
index 9daae69d2a..a929b6d1e4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java
@@ -71,8 +71,8 @@ public class JpaRpcDao extends JpaAbstractDao implements RpcDao,
@Transactional
@Override
- public int deleteOutdatedRpcByTenantId(TenantId tenantId, Long expirationTime) {
- return rpcRepository.deleteOutdatedRpcByTenantId(tenantId.getId(), expirationTime);
+ public int deleteOutdatedRpcByTenantIdBatch(TenantId tenantId, Long expirationTime, int batchSize) {
+ return rpcRepository.deleteOutdatedRpcByTenantIdBatch(tenantId.getId(), expirationTime, batchSize);
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java
index 8a9489333f..3f76170c84 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java
@@ -21,20 +21,27 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
+import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.dao.model.sql.RpcEntity;
import java.util.UUID;
public interface RpcRepository extends JpaRepository {
+
Page findAllByTenantIdAndDeviceId(UUID tenantId, UUID deviceId, Pageable pageable);
Page findAllByTenantIdAndDeviceIdAndStatus(UUID tenantId, UUID deviceId, RpcStatus status, Pageable pageable);
Page findAllByTenantId(UUID tenantId, Pageable pageable);
+ @Transactional
@Modifying
- @Query(value = "DELETE FROM rpc WHERE tenant_id = :tenantId AND created_time < :expirationTime",
+ @Query(value = "DELETE FROM rpc WHERE id IN " +
+ "(SELECT id FROM rpc WHERE tenant_id = :tenantId AND created_time < :expirationTime LIMIT :batchSize)",
nativeQuery = true)
- int deleteOutdatedRpcByTenantId(@Param("tenantId") UUID tenantId, @Param("expirationTime") Long expirationTime);
+ int deleteOutdatedRpcByTenantIdBatch(@Param("tenantId") UUID tenantId,
+ @Param("expirationTime") Long expirationTime,
+ @Param("batchSize") int batchSize);
+
}
diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql
index 3d9c9f17e6..d5e359b241 100644
--- a/dao/src/main/resources/sql/schema-entities.sql
+++ b/dao/src/main/resources/sql/schema-entities.sql
@@ -935,6 +935,7 @@ CREATE TABLE IF NOT EXISTS calculated_field (
configuration varchar(1000000),
version BIGINT DEFAULT 1,
debug_settings varchar(1024),
+ additional_info varchar,
CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name)
);
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
index c9524ba7b5..3caf4bb5f4 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.dao.service;
import org.apache.commons.lang3.RandomUtils;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.TenantProfile;
@@ -83,6 +84,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration());
+ assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
@@ -90,6 +92,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
CalculatedField updatedCalculatedField = calculatedFieldService.save(savedCalculatedField);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
+ assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
}
@@ -495,6 +498,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId));
+ calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
return calculatedField;
}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java
index 002a5a7845..69fa3fbc5d 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java
@@ -36,14 +36,14 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
-import org.thingsboard.server.exception.DataValidationException;
+import org.thingsboard.server.dao.ota.OtaPackageDao;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService;
+import org.thingsboard.server.exception.DataValidationException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -77,6 +77,8 @@ public class OtaPackageServiceTest extends AbstractServiceTest {
@Autowired
TenantProfileService tenantProfileService;
@Autowired
+ OtaPackageDao otaPackageDao;
+ @Autowired
TbTenantProfileCache tenantProfileCache;
@Before
@@ -118,10 +120,8 @@ public class OtaPackageServiceTest extends AbstractServiceTest {
Assert.assertEquals(1, otaPackageService.sumDataSizeByTenantId(tenantId));
int maxSumDataSize = 8;
- List packages = new ArrayList<>(maxSumDataSize);
-
for (int i = 2; i <= maxSumDataSize; i++) {
- packages.add(createAndSaveFirmware(tenantId, "0." + i));
+ createAndSaveFirmware(tenantId, "0." + i);
Assert.assertEquals(i, otaPackageService.sumDataSizeByTenantId(tenantId));
}
@@ -533,6 +533,39 @@ public class OtaPackageServiceTest extends AbstractServiceTest {
Assert.assertNull(foundFirmware);
}
+ @Test
+ public void testDeleteOtaPackageWithoutData() {
+ OtaPackageInfo firmwareInfo = new OtaPackageInfo();
+ firmwareInfo.setTenantId(tenantId);
+ firmwareInfo.setDeviceProfileId(deviceProfileId);
+ firmwareInfo.setType(FIRMWARE);
+ firmwareInfo.setTitle(TITLE);
+ firmwareInfo.setVersion(VERSION);
+ OtaPackageInfo savedFirmwareInfo = otaPackageService.saveOtaPackageInfo(firmwareInfo, false);
+
+ Assert.assertNotNull(savedFirmwareInfo);
+ Assert.assertNotNull(savedFirmwareInfo.getId());
+
+ // Should not throw NPE when deleting package without data (OID is null)
+ otaPackageService.deleteOtaPackage(tenantId, savedFirmwareInfo.getId());
+
+ OtaPackageInfo foundFirmware = otaPackageService.findOtaPackageInfoById(tenantId, savedFirmwareInfo.getId());
+ Assert.assertNull(foundFirmware);
+ }
+
+ @Test
+ public void testDeleteOtaPackageUnlinksLargeObject() {
+ OtaPackage savedFirmware = createAndSaveFirmware(tenantId, VERSION);
+
+ Long oid = otaPackageDao.getDataOidById(savedFirmware.getId().getId());
+ Assert.assertNotNull(oid);
+
+ otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId());
+
+ // Verify the large object was unlinked - PostgreSQL throws an exception when the object doesn't exist
+ assertThatThrownBy(() -> otaPackageDao.unlinkLargeObject(oid)).hasMessageContaining("large object " + oid + " does not exist");
+ }
+
@Test
public void testFindTenantFirmwaresByTenantId() {
List firmwares = new ArrayList<>();
@@ -567,8 +600,8 @@ public class OtaPackageServiceTest extends AbstractServiceTest {
}
} while (pageData.hasNext());
- Collections.sort(firmwares, idComparator);
- Collections.sort(loadedFirmwares, idComparator);
+ firmwares.sort(idComparator);
+ loadedFirmwares.sort(idComparator);
assertThat(firmwares).isEqualTo(loadedFirmwares);
@@ -622,8 +655,8 @@ public class OtaPackageServiceTest extends AbstractServiceTest {
}
} while (pageData.hasNext());
- Collections.sort(firmwares, idComparator);
- Collections.sort(loadedFirmwares, idComparator);
+ firmwares.sort(idComparator);
+ loadedFirmwares.sort(idComparator);
assertThat(firmwares).isEqualTo(loadedFirmwares);
@@ -726,4 +759,5 @@ public class OtaPackageServiceTest extends AbstractServiceTest {
firmware.setDataSize(DATA_SIZE);
return firmware;
}
+
}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java
index aa43826734..b66cdb4363 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java
@@ -65,7 +65,7 @@ public class TimeseriesServiceNoSqlTest extends BaseTimeseriesServiceTest {
new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(5), new JsonDataEntry("test", "{\"test\":\"testValue\"}")));
DeviceId deviceId = new DeviceId(Uuids.timeBased());
- tsService.save(tenantId, deviceId, timeseries, ttlInSec);
+ tsService.save(tenantId, deviceId, timeseries, ttlInSec).get(MAX_TIMEOUT, TimeUnit.SECONDS);
List fullList = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("test", 0L,
TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS);
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRequestDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRequestDaoTest.java
new file mode 100644
index 0000000000..5e2fe2308b
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRequestDaoTest.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.dao.sql.notification;
+
+import org.junit.After;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.server.common.data.id.NotificationRequestId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.notification.NotificationRequest;
+import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
+import org.thingsboard.server.dao.AbstractJpaDaoTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class JpaNotificationRequestDaoTest extends AbstractJpaDaoTest {
+
+ @Autowired
+ JpaNotificationRequestDao notificationRequestDao;
+
+ private final List createdRequests = new ArrayList<>();
+
+ @After
+ public void tearDown() {
+ for (NotificationRequest request : createdRequests) {
+ notificationRequestDao.removeById(request.getTenantId(), request.getId().getId());
+ }
+ createdRequests.clear();
+ }
+
+ @Test
+ public void testBatchDeletion() {
+ TenantId sysTenantId = TenantId.SYS_TENANT_ID;
+ long now = System.currentTimeMillis();
+ long oldTimestamp = now - TimeUnit.DAYS.toMillis(30);
+
+ NotificationRequest oldRequest1 = createNotificationRequest(sysTenantId, oldTimestamp);
+ notificationRequestDao.save(sysTenantId, oldRequest1);
+
+ NotificationRequest oldRequest2 = createNotificationRequest(sysTenantId, oldTimestamp);
+ notificationRequestDao.save(sysTenantId, oldRequest2);
+
+ NotificationRequest freshRequest = createNotificationRequest(sysTenantId, now);
+ notificationRequestDao.save(sysTenantId, freshRequest);
+
+ TenantId tenant2Id = TenantId.fromUUID(UUID.fromString("3d193a7a-774b-4c05-84d5-f7fdcf7a37cf"));
+ NotificationRequest tenant2Request = createNotificationRequest(tenant2Id, oldTimestamp);
+ notificationRequestDao.save(tenant2Id, tenant2Request);
+
+ int batchSize = 10_000;
+
+ assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(sysTenantId, oldTimestamp - 1, batchSize)).isEqualTo(0);
+
+ long expirationTime = now - TimeUnit.DAYS.toMillis(15);
+ assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(sysTenantId, expirationTime, batchSize)).isEqualTo(2);
+
+ assertThat(notificationRequestDao.findById(sysTenantId, freshRequest.getId().getId())).isNotNull();
+ assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenant2Id, now + 1, batchSize)).isEqualTo(1);
+ }
+
+ @Test
+ public void testBatchDeletionWithSmallBatchSize() {
+ TenantId tenantId = TenantId.SYS_TENANT_ID;
+ long oldTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30);
+
+ for (int i = 0; i < 10; i++) {
+ NotificationRequest request = createNotificationRequest(tenantId, oldTimestamp);
+ notificationRequestDao.save(tenantId, request);
+ }
+
+ int batchSize = 3;
+ long expirationTime = System.currentTimeMillis();
+
+ assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(3);
+ assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(3);
+ assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(3);
+ assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(1);
+ assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(0);
+ }
+
+ @Test
+ public void testBatchDeletionIsolationBetweenTenants() {
+ TenantId tenant1 = TenantId.SYS_TENANT_ID;
+ TenantId tenant2 = TenantId.fromUUID(UUID.fromString("3d193a7a-774b-4c05-84d5-f7fdcf7a37cf"));
+ long oldTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30);
+
+ for (int i = 0; i < 5; i++) {
+ NotificationRequest request = createNotificationRequest(tenant1, oldTimestamp);
+ notificationRequestDao.save(tenant1, request);
+ }
+
+ for (int i = 0; i < 3; i++) {
+ NotificationRequest request = createNotificationRequest(tenant2, oldTimestamp);
+ notificationRequestDao.save(tenant2, request);
+ }
+
+ int batchSize = 10_000;
+ long expirationTime = System.currentTimeMillis();
+
+ assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenant1, expirationTime, batchSize)).isEqualTo(5);
+ assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenant2, expirationTime, batchSize)).isEqualTo(3);
+ }
+
+ private NotificationRequest createNotificationRequest(TenantId tenantId, long createdTime) {
+ NotificationRequest request = new NotificationRequest();
+ request.setId(new NotificationRequestId(UUID.randomUUID()));
+ request.setTenantId(tenantId);
+ request.setCreatedTime(createdTime);
+ request.setTargets(List.of(UUID.randomUUID()));
+ request.setStatus(NotificationRequestStatus.SENT);
+ createdRequests.add(request);
+ return request;
+ }
+
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDaoTest.java
index 1629922685..921339a92b 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDaoTest.java
@@ -51,9 +51,10 @@ public class JpaRpcDaoTest extends AbstractJpaDaoTest {
rpc.setDeviceId(new DeviceId(UUID.randomUUID()));
rpcDao.saveAndFlush(rpc.getTenantId(), rpc);
- assertThat(rpcDao.deleteOutdatedRpcByTenantId(TenantId.SYS_TENANT_ID, 0L)).isEqualTo(0);
- assertThat(rpcDao.deleteOutdatedRpcByTenantId(TenantId.SYS_TENANT_ID, Long.MAX_VALUE)).isEqualTo(2);
- assertThat(rpcDao.deleteOutdatedRpcByTenantId(tenantId, System.currentTimeMillis() + 1)).isEqualTo(1);
+ int batchSize = 10_000;
+ assertThat(rpcDao.deleteOutdatedRpcByTenantIdBatch(TenantId.SYS_TENANT_ID, 0L, batchSize)).isEqualTo(0);
+ assertThat(rpcDao.deleteOutdatedRpcByTenantIdBatch(TenantId.SYS_TENANT_ID, Long.MAX_VALUE, batchSize)).isEqualTo(2);
+ assertThat(rpcDao.deleteOutdatedRpcByTenantIdBatch(tenantId, System.currentTimeMillis() + 1, batchSize)).isEqualTo(1);
}
}
diff --git a/docker/monitoring/grafana/provisioning/dashboards/transport_connections.json b/docker/monitoring/grafana/provisioning/dashboards/transport_connections.json
new file mode 100644
index 0000000000..f092315e94
--- /dev/null
+++ b/docker/monitoring/grafana/provisioning/dashboards/transport_connections.json
@@ -0,0 +1,458 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": null,
+ "links": [],
+ "panels": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "dark-orange",
+ "value": null
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Total connections"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "light-orange",
+ "mode": "fixed"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 15,
+ "w": 19,
+ "x": 0,
+ "y": 0
+ },
+ "id": 19,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "exemplar": false,
+ "expr": "sum by (protocol) (thingsboard_transport_connections_active{})",
+ "format": "time_series",
+ "instant": false,
+ "interval": "",
+ "intervalFactor": 1,
+ "legendFormat": "{{protocol}}",
+ "refId": "A"
+ }
+ ],
+ "title": "Active Connections by Protocol",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "rgba(209, 138, 78, 1)",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 15,
+ "w": 5,
+ "x": 19,
+ "y": 0
+ },
+ "id": 21,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "percentChangeColorMode": "standard",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "showPercentChange": false,
+ "text": {},
+ "textMode": "auto",
+ "wideLayout": true
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "exemplar": true,
+ "expr": "sum (thingsboard_transport_connections_active)",
+ "instant": true,
+ "interval": "",
+ "legendFormat": "",
+ "refId": "A"
+ }
+ ],
+ "title": "Total Connections",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 15,
+ "w": 19,
+ "x": 0,
+ "y": 15
+ },
+ "id": 22,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "exemplar": true,
+ "expr": "thingsboard_transport_connections_active",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 1,
+ "legendFormat": "{{protocol}} {{pod_name}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Active Connections by Pod",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "custom": {
+ "align": "auto",
+ "cellOptions": {
+ "type": "auto"
+ },
+ "filterable": false,
+ "inspect": false
+ },
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "rgba(209, 138, 78, 1)",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Connections"
+ },
+ "properties": [
+ {
+ "id": "custom.cellOptions",
+ "value": {
+ "mode": "gradient",
+ "type": "gauge"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 15,
+ "w": 5,
+ "x": 19,
+ "y": 15
+ },
+ "id": 23,
+ "options": {
+ "cellHeight": "sm",
+ "footer": {
+ "countRows": false,
+ "fields": "",
+ "reducer": [
+ "sum"
+ ],
+ "show": false
+ },
+ "showHeader": true,
+ "sortBy": []
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "exemplar": true,
+ "expr": "sum by (pod_name) (thingsboard_transport_connections_active)",
+ "format": "table",
+ "instant": true,
+ "interval": "",
+ "legendFormat": "__auto",
+ "queryType": "randomWalk",
+ "refId": "A"
+ }
+ ],
+ "title": "Connection Count by Pod",
+ "transformations": [
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": {
+ "Time": true,
+ "__name__": true,
+ "app": true,
+ "controller_revision_hash": true,
+ "instance": true,
+ "job": true,
+ "kubernetes_name": true,
+ "kubernetes_namespace": true,
+ "node_name": true,
+ "pod_name": false,
+ "statefulset_kubernetes_io_pod_name": true
+ },
+ "indexByName": {},
+ "renameByName": {
+ "Time": "",
+ "Value": "Connections",
+ "pod_name": "Pod"
+ }
+ }
+ },
+ {
+ "id": "sortBy",
+ "options": {
+ "fields": {},
+ "sort": [
+ {
+ "field": "Pod"
+ }
+ ]
+ }
+ }
+ ],
+ "type": "table"
+ }
+ ],
+ "refresh": "30s",
+ "schemaVersion": 39,
+ "tags": [
+ "thingsboard"
+ ],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-1h",
+ "to": "now"
+ },
+ "timepicker": {
+ "refresh_intervals": [
+ "10s",
+ "30s",
+ "1m",
+ "5m",
+ "15m",
+ "30m",
+ "1h",
+ "2h",
+ "1d"
+ ]
+ },
+ "timezone": "",
+ "title": "Transport Connections",
+ "uid": "lewbrlwjerwkj3",
+ "version": 1,
+ "weekStart": ""
+}
diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml
index 010994dc88..93914fffbe 100644
--- a/edqs/src/main/resources/edqs.yml
+++ b/edqs/src/main/resources/edqs.yml
@@ -25,19 +25,19 @@ app:
# Application version
version: "@project.version@"
-# Zookeeper connection parameters
+# ZooKeeper connection parameters
zk:
- # Enable/disable zookeeper discovery service.
+ # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:true}"
- # Zookeeper connect string
+ # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}"
- # Zookeeper retry interval in milliseconds
+ # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
- # Zookeeper connection timeout in milliseconds
+ # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
- # Zookeeper session timeout in milliseconds
+ # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
- # Name of the directory in zookeeper 'filesystem'
+ # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
diff --git a/msa/js-executor/config/default.yml b/msa/js-executor/config/default.yml
index be49ece5a6..3a08495069 100644
--- a/msa/js-executor/config/default.yml
+++ b/msa/js-executor/config/default.yml
@@ -14,46 +14,56 @@
# limitations under the License.
#
-queue_type: "kafka"
-request_topic: "js_eval.requests"
-queue_prefix: ""
-http_port: "8888" # /livenessProbe
+# Queue configuration
+# Defines the messaging queue backend used to dispatch JS evaluation requests to the executor and receive results.
+queue_type: "kafka" # Queue type used for JS evaluation requests: kafka or in-memory
+request_topic: "js_eval.requests" # Kafka topic name for incoming JS evaluation requests
+queue_prefix: "" # Optional prefix applied to all queue/topic names; useful for environment isolation
+http_port: "8888" # HTTP port exposing the /livenessProbe health check endpoint
+# JavaScript execution configuration
+# Controls response polling behavior and output size limits for individual JS script executions.
js:
- response_poll_interval: "25"
- max_result_size: "300000"
+ response_poll_interval: "25" # Interval in milliseconds to poll for JS execution responses
+ max_result_size: "300000" # Maximum allowed size in bytes of a JS execution result
+# Kafka configuration
+# Connection, producer, and topic settings for the Kafka message broker used as the JS evaluation queue.
kafka:
bootstrap:
# Kafka Bootstrap Servers
servers: "localhost:9092"
- replication_factor: "1"
+ replication_factor: "1" # Number of replicas for each Kafka topic partition
acks: "1" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge
- batch_size: "128" # for producer
- linger_ms: "5" # for producer
+ batch_size: "128" # Maximum number of messages to accumulate before triggering a batch send
+ linger_ms: "5" # Time in milliseconds the producer waits before sending a batch, allowing more messages to accumulate
partitions_consumed_concurrently: "1" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency
- requestTimeout: "30000" # The default value in kafkajs is: 30000
- connectionTimeout: "1000" # The default value in kafkajs is: 1000
- compression: "none" # gzip or uncompressed
- topic_properties: "retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1"
- use_confluent_cloud: false
- client_id: "kafkajs" #inject pod name to easy identify the client using /opt/kafka/bin/kafka-consumer-groups.sh
+ requestTimeout: "30000" # Time in milliseconds to wait for a response to a Kafka request before timing out
+ connectionTimeout: "1000" # Time in milliseconds to wait when establishing a connection to a Kafka broker
+ compression: "none" # Message compression codec for the producer: none, gzip
+ topic_properties: "retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1" # Semicolon-separated Kafka topic configuration properties applied on topic creation
+ use_confluent_cloud: false # Set to true to enable Confluent Cloud-specific configuration (SSL + SASL)
+ client_id: "kafkajs" # Kafka client identifier; inject the pod name to easily identify the client via kafka-consumer-groups.sh
ssl:
- enabled: false
+ enabled: false # Enable SSL/TLS for Kafka broker connections
confluent:
sasl:
- mechanism: "PLAIN"
+ mechanism: "PLAIN" # SASL mechanism for Confluent Cloud authentication: PLAIN, SCRAM-SHA-256, or SCRAM-SHA-512
+# Logging configuration
+# Controls log verbosity, output directory, and log file naming pattern for the JS executor service.
logger:
- level: "info"
- path: "logs"
- filename: "tb-js-executor-%DATE%.log"
+ level: "info" # Log level: trace, debug, info, warn, or error
+ path: "logs" # Directory path where log files are written
+ filename: "tb-js-executor-%DATE%.log" # Log file name pattern; %DATE% is replaced with the current date
+# JavaScript execution and monitoring configuration
+# Sandboxing, script caching, and observability settings for the JS script runtime.
script:
- use_sandbox: "true"
- memory_usage_trace_frequency: "1000"
- script_body_trace_frequency: "10000"
- stat_print_frequency: "10000"
- max_active_scripts: "1000"
- slow_query_log_ms: "5.000000" #millis
- slow_query_log_body: "false"
+ use_sandbox: "true" # Run scripts inside an isolated sandbox to prevent access to Node.js internals
+ memory_usage_trace_frequency: "1000" # Log memory usage every N script executions
+ script_body_trace_frequency: "10000" # Log the script body every N script executions
+ stat_print_frequency: "10000" # Print execution statistics every N script executions
+ max_active_scripts: "1000" # Maximum number of compiled scripts kept in the cache
+ slow_query_log_ms: "5.000000" # Execution time threshold in milliseconds above which a script is considered slow and logged
+ slow_query_log_body: "false" # Log the script body when execution time exceeds slow_query_log_ms threshold
diff --git a/msa/js-executor/yarn.lock b/msa/js-executor/yarn.lock
index 6457be1de7..08a789e9b5 100644
--- a/msa/js-executor/yarn.lock
+++ b/msa/js-executor/yarn.lock
@@ -1036,9 +1036,9 @@ mimic-response@^3.1.0:
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@^3.1.2:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624"
- integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
+ integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
dependencies:
brace-expansion "^1.1.7"
@@ -1549,9 +1549,9 @@ tar-stream@^2.1.4:
readable-stream "^3.1.1"
tar@>=7.5.8, tar@^7.4.3:
- version "7.5.9"
- resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.9.tgz#817ac12a54bc4362c51340875b8985d7dc9724b8"
- integrity sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==
+ version "7.5.11"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.11.tgz#1250fae45d98806b36d703b30973fa8e0a6d8868"
+ integrity sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==
dependencies:
"@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0"
diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml
index 56e15d4b4d..188583dc7e 100644
--- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml
+++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml
@@ -15,30 +15,33 @@
#
# Spring common parameters
+# Controls Spring Boot startup mode, web environment type, and bean wiring behaviour.
spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'.
spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
# Server common parameters
+# Configures the embedded HTTP server bind address and port used for actuator and metrics endpoints.
server:
# Server bind address (has no effect if web-environment is disabled).
address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
# Server bind port (has no effect if web-environment is disabled).
port: "${HTTP_BIND_PORT:8086}"
-# Zookeeper connection parameters. Used for service discovery.
+# ZooKeeper connection parameters
+# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments.
zk:
- # Enable/disable zookeeper discovery service.
+ # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:true}"
- # Zookeeper connect string
+ # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}"
- # Zookeeper retry interval in milliseconds
+ # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
- # Zookeeper connection timeout in milliseconds
+ # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
- # Zookeeper session timeout in milliseconds
+ # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
- # Name of the directory in zookeeper 'filesystem'
+ # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@@ -46,6 +49,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Queue configuration parameters
+# Defines the message queue type (Kafka or in-memory) and all related producer, consumer, and topic settings.
queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
@@ -196,6 +200,7 @@ queue:
msg-chunk-size: "${TB_QUEUE_VC_MSG_CHUNK_SIZE:250000}"
# Version control parameters
+# Configures the Git-based version control executor, including thread pools and local repository storage.
vc:
# Pool size for handling export tasks
thread_pool_size: "${TB_VC_POOL_SIZE:6}"
@@ -206,6 +211,7 @@ vc:
repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}"
# Usage statistics parameters
+# Controls collection and reporting of API usage statistics at system, tenant, and customer levels.
usage:
stats:
report:
@@ -221,6 +227,7 @@ usage:
pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}"
# Metrics parameters
+# Enables actuator-based metrics collection and configures histogram/timer percentile exports.
metrics:
# Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}"
@@ -229,6 +236,7 @@ metrics:
percentiles: "${METRICS_TIMER_PERCENTILES:0.5}"
# General management parameters
+# Configures Spring Boot Actuator endpoint exposure, including the metrics and info endpoints.
management:
endpoints:
web:
@@ -237,12 +245,14 @@ management:
include: "${METRICS_ENDPOINTS_EXPOSE:info}"
# Service common properties
+# Identifies this microservice instance by type and optional unique ID for cluster coordination.
service:
type: "${TB_SERVICE_TYPE:tb-vc-executor}" # service type
# Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}"
# Notification system parameters
+# Defines deduplication rules for notification triggers to prevent repeated alerts within a time window.
notification_system:
rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'
diff --git a/msa/web-ui/config/custom-environment-variables.yml b/msa/web-ui/config/custom-environment-variables.yml
index 90bce53cb4..6ba9147555 100644
--- a/msa/web-ui/config/custom-environment-variables.yml
+++ b/msa/web-ui/config/custom-environment-variables.yml
@@ -25,6 +25,20 @@ thingsboard:
host: "TB_HOST"
# ThingsBoard node port
port: "TB_PORT"
+security:
+ headers:
+ x-content-type-options:
+ enabled: "SECURITY_HEADERS_X_CONTENT_TYPE_OPTIONS_ENABLED"
+ referrer-policy:
+ enabled: "SECURITY_HEADERS_REFERRER_POLICY_ENABLED"
+ value: "SECURITY_HEADERS_REFERRER_POLICY_VALUE"
+ x-frame-options:
+ enabled: "SECURITY_HEADERS_X_FRAME_OPTIONS_ENABLED"
+ value: "SECURITY_HEADERS_X_FRAME_OPTIONS_VALUE"
+ content-security-policy:
+ enabled: "SECURITY_HEADERS_CONTENT_SECURITY_POLICY_ENABLED"
+ value: "SECURITY_HEADERS_CONTENT_SECURITY_POLICY_VALUE"
+ report-only: "SECURITY_HEADERS_CONTENT_SECURITY_POLICY_REPORT_ONLY"
logger:
level: "LOGGER_LEVEL"
path: "LOG_FOLDER"
diff --git a/msa/web-ui/config/default.yml b/msa/web-ui/config/default.yml
index b26424a8da..6ffa85b3bc 100644
--- a/msa/web-ui/config/default.yml
+++ b/msa/web-ui/config/default.yml
@@ -25,6 +25,20 @@ thingsboard:
host: "localhost"
# ThingsBoard node port
port: "8080"
+security:
+ headers:
+ x-content-type-options:
+ enabled: true
+ referrer-policy:
+ enabled: true
+ value: "strict-origin-when-cross-origin"
+ x-frame-options:
+ enabled: false
+ value: "SAMEORIGIN"
+ content-security-policy:
+ enabled: false
+ value: ""
+ report-only: false
logger:
level: "info"
path: "logs"
diff --git a/msa/web-ui/server.ts b/msa/web-ui/server.ts
index 68baf5079f..0a4ddbfa6b 100644
--- a/msa/web-ui/server.ts
+++ b/msa/web-ui/server.ts
@@ -60,6 +60,36 @@ let connections: Socket[] = [];
const app = express();
server = http.createServer(app);
+ // Build security headers map once at startup.
+ // node-config passes env var overrides as strings, so enabled can be boolean or string.
+ const isEnabled = (val: any) => val === true || val === 'true';
+ const securityHeaders: Record = {};
+ const hc: any = config.get('security.headers');
+ if (isEnabled(hc['x-content-type-options']?.enabled)) {
+ securityHeaders['X-Content-Type-Options'] = 'nosniff';
+ }
+ if (isEnabled(hc['referrer-policy']?.enabled)) {
+ securityHeaders['Referrer-Policy'] = hc['referrer-policy']?.value || 'strict-origin-when-cross-origin';
+ }
+ if (isEnabled(hc['x-frame-options']?.enabled)) {
+ securityHeaders['X-Frame-Options'] = hc['x-frame-options']?.value || 'SAMEORIGIN';
+ }
+ if (isEnabled(hc['content-security-policy']?.enabled) && hc['content-security-policy']?.value) {
+ const csp = hc['content-security-policy'];
+ const name = isEnabled(csp['report-only'])
+ ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
+ securityHeaders[name] = csp.value;
+ }
+ logger.info('Security headers: %s', JSON.stringify(securityHeaders));
+
+ // Apply security headers to all responses
+ app.use((_req, res, next) => {
+ for (const [name, value] of Object.entries(securityHeaders)) {
+ res.setHeader(name, value);
+ }
+ next();
+ });
+
let apiProxy: httpProxy;
if (useApiProxy) {
apiProxy = httpProxy.createProxyServer({
diff --git a/msa/web-ui/yarn.lock b/msa/web-ui/yarn.lock
index 11a708d8bf..52dd2a03f3 100644
--- a/msa/web-ui/yarn.lock
+++ b/msa/web-ui/yarn.lock
@@ -1098,9 +1098,9 @@ mimic-response@^3.1.0:
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@^3.1.2:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624"
- integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
+ integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
dependencies:
brace-expansion "^1.1.7"
@@ -1631,9 +1631,9 @@ tar-stream@^2.1.4:
readable-stream "^3.1.1"
tar@>=7.5.8, tar@^7.4.3:
- version "7.5.9"
- resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.9.tgz#817ac12a54bc4362c51340875b8985d7dc9724b8"
- integrity sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==
+ version "7.5.11"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.11.tgz#1250fae45d98806b36d703b30973fa8e0a6d8868"
+ integrity sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==
dependencies:
"@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0"
diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml
index 6856da9233..c23aaabf80 100644
--- a/netty-mqtt/pom.xml
+++ b/netty-mqtt/pom.xml
@@ -125,7 +125,6 @@
org.apache.maven.plugins
maven-jar-plugin
- 3.1.1
diff --git a/pom.xml b/pom.xml
index bf125bf209..4dcc3929fd 100755
--- a/pom.xml
+++ b/pom.xml
@@ -63,7 +63,7 @@
2.0.0-M15
2.0.1
5.6.0
- 3.9.3
+ 3.9.5
3.25.5
1.76.0
1.2.9
@@ -72,7 +72,7 @@
1.2.5
1.7.1
3.5.4
- 3.4.0
+ 3.5.0
2.8.8TB
2.2.30
0.8
@@ -122,6 +122,7 @@
0.27.0
1.7.0
+ 3.27.7
2.7.3
1.5.6
5.15.0
@@ -533,6 +534,8 @@
-PpkgInstallFolder=${pkg.installFolder}
-PpkgCopyInstallScripts=${pkg.copyInstallScripts}
-PpkgLogFolder=${pkg.unixLogFolder}
+ --project-cache-dir
+ ${project.build.directory}/.gradle
--warning-mode
all
@@ -636,7 +639,7 @@
org.apache.maven.plugins
maven-jar-plugin
- 3.1.1
+ ${jar-plugin.version}
org.apache.maven.plugins
@@ -890,6 +893,21 @@
com.mycila
license-maven-plugin
+
+ org.apache.maven.plugins
+ maven-clean-plugin
+ false
+
+
+
+ ${main.dir}/packaging/java/.gradle
+
+
+ ${main.dir}/packaging/js/.gradle
+
+
+
+
@@ -931,6 +949,15 @@
+
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
+
+
+
org.springframework.boot
spring-boot-dependencies
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroup.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroup.java
new file mode 100644
index 0000000000..9d15cb9793
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroup.java
@@ -0,0 +1,174 @@
+/**
+ * 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.rule.engine.rest;
+
+import io.netty.resolver.AddressResolver;
+import io.netty.resolver.AddressResolverGroup;
+import io.netty.resolver.DefaultAddressResolverGroup;
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.Promise;
+import org.thingsboard.common.util.SsrfProtectionValidator;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Custom Netty {@link AddressResolverGroup} that validates every resolved IP address
+ * against the SSRF block-list at connection time. This eliminates the DNS rebinding
+ * TOCTOU gap where a hostname resolves to a safe IP during validation but to a
+ * private/metadata IP when the actual connection is made.
+ *
+ * Only wired into {@link TbHttpClient} when SSRF protection is enabled.
+ */
+public final class SsrfSafeAddressResolverGroup extends AddressResolverGroup {
+
+ public static final SsrfSafeAddressResolverGroup INSTANCE = new SsrfSafeAddressResolverGroup();
+
+ private SsrfSafeAddressResolverGroup() {
+ }
+
+ @Override
+ protected AddressResolver newResolver(EventExecutor executor) throws Exception {
+ AddressResolver delegate = DefaultAddressResolverGroup.INSTANCE.getResolver(executor);
+ return new SsrfValidatingResolver(executor, delegate);
+ }
+
+ private static final class SsrfValidatingResolver implements AddressResolver {
+
+ private final EventExecutor executor;
+ private final AddressResolver delegate;
+
+ SsrfValidatingResolver(EventExecutor executor, AddressResolver delegate) {
+ this.executor = executor;
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean isSupported(SocketAddress address) {
+ return delegate.isSupported(address);
+ }
+
+ @Override
+ public boolean isResolved(SocketAddress address) {
+ return delegate.isResolved(address);
+ }
+
+ @Override
+ public Future resolve(SocketAddress address) {
+ return resolve(address, executor.newPromise());
+ }
+
+ @Override
+ public Future resolve(SocketAddress address, Promise promise) {
+ delegate.resolve(address).addListener((Future future) -> {
+ try {
+ if (!future.isSuccess()) {
+ promise.tryFailure(future.cause());
+ return;
+ }
+ InetSocketAddress resolved = future.getNow();
+ if (isOriginalHostAllowed(address)) {
+ promise.trySuccess(resolved);
+ } else if (isBlocked(resolved)) {
+ promise.tryFailure(new RuntimeException(
+ "URI is invalid: host '" + getHostString(address) + "' is not allowed"));
+ } else {
+ promise.trySuccess(resolved);
+ }
+ } catch (Exception e) {
+ promise.tryFailure(e);
+ }
+ });
+ return promise;
+ }
+
+ @Override
+ public Future> resolveAll(SocketAddress address) {
+ return resolveAll(address, executor.newPromise());
+ }
+
+ @Override
+ public Future> resolveAll(SocketAddress address, Promise> promise) {
+ delegate.resolveAll(address).addListener((Future> future) -> {
+ try {
+ if (!future.isSuccess()) {
+ promise.tryFailure(future.cause());
+ return;
+ }
+ List resolved = future.getNow();
+ if (isOriginalHostAllowed(address)) {
+ promise.trySuccess(resolved);
+ return;
+ }
+ Set blocked = null;
+ for (InetSocketAddress addr : resolved) {
+ if (isBlocked(addr)) {
+ if (blocked == null) {
+ blocked = new HashSet<>(2);
+ }
+ blocked.add(addr);
+ }
+ }
+ if (blocked == null) {
+ promise.trySuccess(resolved);
+ } else if (blocked.size() == resolved.size()) {
+ promise.tryFailure(new RuntimeException(
+ "URI is invalid: host '" + getHostString(address) + "' is not allowed"));
+ } else {
+ List safe = new ArrayList<>(resolved.size() - blocked.size());
+ for (InetSocketAddress addr : resolved) {
+ if (!blocked.contains(addr)) {
+ safe.add(addr);
+ }
+ }
+ promise.trySuccess(safe);
+ }
+ } catch (Exception e) {
+ promise.tryFailure(e);
+ }
+ });
+ return promise;
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+
+ private static boolean isBlocked(InetSocketAddress socketAddress) {
+ InetAddress addr = socketAddress.getAddress();
+ return addr != null && SsrfProtectionValidator.isBlockedAddress(addr);
+ }
+
+ private static boolean isOriginalHostAllowed(SocketAddress address) {
+ if (address instanceof InetSocketAddress isa) {
+ String host = isa.getHostString();
+ return host != null && SsrfProtectionValidator.isHostnameAllowed(host);
+ }
+ return false;
+ }
+
+ private static String getHostString(SocketAddress address) {
+ return address instanceof InetSocketAddress isa ? isa.getHostString() : address.toString();
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
index 24f88e88a3..df9ce0194b 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
@@ -103,6 +103,7 @@ public class TbHttpClient {
.build();
HttpClient httpClient = HttpClient.create(connectionProvider)
+ .followRedirect(false)
.runOn(getSharedOrCreateEventLoopGroup(eventLoopGroupShared))
.doOnConnected(c ->
c.addHandlerLast(new ReadTimeoutHandler(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS)));
@@ -138,6 +139,10 @@ public class TbHttpClient {
httpClient = httpClient.secure(t -> t.sslContext(sslContext));
}
+ if (SsrfProtectionValidator.isEnabled()) {
+ httpClient = httpClient.resolver(SsrfSafeAddressResolverGroup.INSTANCE);
+ }
+
validateMaxInMemoryBufferSize(config);
this.webClient = WebClient.builder()
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroupTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroupTest.java
new file mode 100644
index 0000000000..99506d3d12
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/SsrfSafeAddressResolverGroupTest.java
@@ -0,0 +1,161 @@
+/**
+ * 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.rule.engine.rest;
+
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.resolver.AddressResolver;
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.Promise;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.thingsboard.common.util.SsrfProtectionValidator;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@ResourceLock("SsrfSafeAddressResolverGroupTest")
+class SsrfSafeAddressResolverGroupTest {
+
+ private static NioEventLoopGroup eventLoopGroup;
+
+ @BeforeAll
+ static void setUp() {
+ eventLoopGroup = new NioEventLoopGroup(1);
+ }
+
+ @AfterAll
+ static void tearDown() {
+ eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
+ SsrfProtectionValidator.setEnabled(false);
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+
+ @BeforeEach
+ void enableSsrf() {
+ SsrfProtectionValidator.setEnabled(true);
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ }
+
+ @AfterEach
+ void resetState() {
+ SsrfProtectionValidator.setAllowedHosts(Collections.emptyList());
+ SsrfProtectionValidator.setEnabled(false);
+ }
+
+ @Test
+ void isBlockedAddressWorksForLoopback() throws Exception {
+ assertThat(SsrfProtectionValidator.isBlockedAddress(InetAddress.getByName("127.0.0.1"))).isTrue();
+ assertThat(SsrfProtectionValidator.isBlockedAddress(InetAddress.getByName("192.168.1.1"))).isTrue();
+ assertThat(SsrfProtectionValidator.isBlockedAddress(InetAddress.getByName("8.8.8.8"))).isFalse();
+ }
+
+ @Test
+ void resolvePublicIpSucceeds() throws Exception {
+ EventExecutor executor = eventLoopGroup.next();
+ AddressResolver resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
+ Promise promise = executor.newPromise();
+
+ executor.submit(() -> resolver.resolve(InetSocketAddress.createUnresolved("8.8.8.8", 80), promise));
+ InetSocketAddress result = promise.get(10, TimeUnit.SECONDS);
+
+ assertThat(result.getAddress()).isNotNull();
+ assertThat(result.getAddress().getHostAddress()).isEqualTo("8.8.8.8");
+ }
+
+ @Test
+ void resolveLoopbackFailsWhenSsrfEnabled() throws Exception {
+ assertThat(SsrfProtectionValidator.isEnabled()).isTrue();
+
+ EventExecutor executor = eventLoopGroup.next();
+ AddressResolver resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
+ Promise promise = executor.newPromise();
+
+ executor.submit(() -> resolver.resolve(InetSocketAddress.createUnresolved("127.0.0.1", 80), promise));
+
+ assertThatThrownBy(() -> promise.get(10, TimeUnit.SECONDS))
+ .isInstanceOf(ExecutionException.class)
+ .hasRootCauseInstanceOf(RuntimeException.class)
+ .rootCause().hasMessageContaining("is not allowed");
+ }
+
+ @Test
+ void resolvePrivateIpFailsWhenSsrfEnabled() throws Exception {
+ assertThat(SsrfProtectionValidator.isEnabled()).isTrue();
+
+ EventExecutor executor = eventLoopGroup.next();
+ AddressResolver resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
+ Promise promise = executor.newPromise();
+
+ executor.submit(() -> resolver.resolve(InetSocketAddress.createUnresolved("192.168.1.1", 80), promise));
+
+ assertThatThrownBy(() -> promise.get(10, TimeUnit.SECONDS))
+ .isInstanceOf(ExecutionException.class)
+ .hasRootCauseInstanceOf(RuntimeException.class)
+ .rootCause().hasMessageContaining("is not allowed");
+ }
+
+ @Test
+ void resolveAllowedPrivateIpSucceeds() throws Exception {
+ SsrfProtectionValidator.setAllowedHosts(List.of("192.168.1.0/24"));
+
+ EventExecutor executor = eventLoopGroup.next();
+ AddressResolver resolver = SsrfSafeAddressResolverGroup.INSTANCE.getResolver(executor);
+ Promise