diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 02ede2edaa..0c3589a077 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -116,7 +116,7 @@ import java.util.stream.Collectors; * @author Andrew Shvayka */ @Slf4j -class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { +public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { static final String SESSION_TIMEOUT_MESSAGE = "session timeout!"; final TenantId tenantId; diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 7b1ce89ccd..0c0ff3e0c9 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -65,7 +65,6 @@ import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceService; @@ -95,6 +94,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MC import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; @@ -290,6 +290,7 @@ public class DefaultTransportApiService implements TransportApiService { device.setType(requestMsg.getDeviceType()); device.setCustomerId(gateway.getCustomerId()); DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType()); + device.setDeviceProfileId(deviceProfile.getId()); ObjectNode additionalInfo = JacksonUtil.newObjectNode(); additionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString()); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index 6c42c7604d..b3025a2d64 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -18,7 +18,10 @@ package org.thingsboard.server.controller; import lombok.extern.slf4j.Slf4j; 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.thingsboard.server.actors.service.ActorService; +import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; @@ -38,6 +41,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.service.session.DeviceSessionCacheService; import java.util.ArrayList; import java.util.List; diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index cadf2c23be..48db5912a0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -26,6 +26,7 @@ import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.hamcrest.Matcher; import org.hibernate.exception.ConstraintViolationException; import org.junit.After; @@ -49,6 +50,7 @@ import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.mock.http.MockHttpInputMessage; import org.springframework.mock.http.MockHttpOutputMessage; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; @@ -57,9 +59,15 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilde import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.context.WebApplicationContext; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.actors.DefaultTbActorSystem; +import org.thingsboard.server.actors.TbActorId; +import org.thingsboard.server.actors.TbActorMailbox; +import org.thingsboard.server.actors.TbEntityActorId; +import org.thingsboard.server.actors.device.DeviceActor; +import org.thingsboard.server.actors.device.DeviceActorMessageProcessor; +import org.thingsboard.server.actors.device.SessionInfo; +import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -81,6 +89,7 @@ import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeCon import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; @@ -93,9 +102,11 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.config.ThingsboardSecurityConfiguration; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; @@ -107,6 +118,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -186,6 +201,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @Autowired private TenantProfileService tenantProfileService; + @Autowired + public TimeseriesService tsService; + + @Autowired + protected DefaultActorService actorService; + @SpyBean protected MailService mailService; @@ -854,4 +875,37 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return (T) field.get(target); } + protected int getDeviceActorSubscriptionCount(DeviceId deviceId, FeatureType featureType) { + DeviceActorMessageProcessor processor = getDeviceActorProcessor(deviceId); + Map subscriptions = (Map) ReflectionTestUtils.getField(processor, getMapName(featureType)); + return subscriptions.size(); + } + + protected void awaitForDeviceActorToReceiveSubscription(DeviceId deviceId, FeatureType featureType, int subscriptionCount) { + DeviceActorMessageProcessor processor = getDeviceActorProcessor(deviceId); + Map subscriptions = (Map) ReflectionTestUtils.getField(processor, getMapName(featureType)); + Awaitility.await("Device actor received subscription command from the transport").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> subscriptions.size() == subscriptionCount); + } + + protected static String getMapName(FeatureType featureType) { + switch (featureType) { + case ATTRIBUTES: + return "attributeSubscriptions"; + case RPC: + return "rpcSubscriptions"; + default: + throw new RuntimeException("Not supported feature " + featureType + "!"); + } + } + + protected DeviceActorMessageProcessor getDeviceActorProcessor(DeviceId deviceId) { + DefaultTbActorSystem actorSystem = (DefaultTbActorSystem) ReflectionTestUtils.getField(actorService, "system"); + ConcurrentMap actors = (ConcurrentMap) ReflectionTestUtils.getField(actorSystem, "actors"); + Awaitility.await("Device actor was created").atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> actors.containsKey(new TbEntityActorId(deviceId))); + TbActorMailbox actorMailbox = actors.get(new TbEntityActorId(deviceId)); + DeviceActor actor = (DeviceActor) ReflectionTestUtils.getField(actorMailbox, "actor"); + return (DeviceActorMessageProcessor) ReflectionTestUtils.getField(actor, "processor"); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java index 777b97d940..6a94cc5a1e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java @@ -405,6 +405,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Alarm alarm = createAlarm(TEST_ALARM_TYPE); Mockito.reset(tbClusterService, auditLogService); long beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); @@ -434,6 +435,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Alarm alarm = createAlarm(TEST_ALARM_TYPE); Mockito.reset(tbClusterService, auditLogService); long beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); @@ -450,6 +452,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { loginCustomerUser(); Mockito.reset(tbClusterService, auditLogService); beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); doPost("/api/alarm/" + alarm.getId() + "/assign/" + customerUserId.getId()).andExpect(status().isOk()); @@ -468,6 +471,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Alarm alarm = createAlarm(TEST_ALARM_TYPE); Mockito.reset(tbClusterService, auditLogService); long beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); @@ -479,7 +483,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); beforeAssignmentTs = System.currentTimeMillis(); - + Thread.sleep(2); doDelete("/api/alarm/" + alarm.getId() + "/assign").andExpect(status().isOk()); foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); Assert.assertNotNull(foundAlarm); @@ -496,6 +500,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Alarm alarm = createAlarm(TEST_ALARM_TYPE); Mockito.reset(tbClusterService, auditLogService); long beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); @@ -511,6 +516,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); doDelete("/api/alarm/" + alarm.getId() + "/assign").andExpect(status().isOk()); foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java index b266465bd1..98508e72ce 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java @@ -49,6 +49,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; @@ -668,6 +669,7 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { client.connectAndWait(deviceCredentials.getCredentialsId()); MqttTestCallback onUpdateCallback = new MqttTestCallback(); client.setCallback(onUpdateCallback); + client.subscribeAndWait("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE); edgeImitator.expectResponsesAmount(1); diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index ce02296733..b0282832bc 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -21,6 +21,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.junit.rules.ExpectedException; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.EntityInfo; @@ -237,7 +238,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resourceService.delete(savedResource, null); } - @Test(expected = DataValidationException.class) + @Test public void testSaveTbResourceWithExistsFileName() throws Exception { TbResource resource = new TbResource(); resource.setTenantId(tenantId); @@ -256,23 +257,27 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setData("Test Data"); try { - resourceService.save(resource2); + Assertions.assertThrows(DataValidationException.class, () -> { + resourceService.save(resource2); + }); } finally { resourceService.delete(savedResource, null); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveTbResourceWithEmptyTitle() throws Exception { TbResource resource = new TbResource(); resource.setTenantId(tenantId); resource.setResourceType(ResourceType.JKS); resource.setFileName(DEFAULT_FILE_NAME); resource.setData("Test Data"); - resourceService.save(resource); + Assertions.assertThrows(DataValidationException.class, () -> { + resourceService.save(resource); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveTbResourceWithInvalidTenant() throws Exception { TbResource resource = new TbResource(); resource.setTenantId(TenantId.fromUUID(Uuids.timeBased())); @@ -280,7 +285,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); resource.setData("Test Data"); - resourceService.save(resource); + Assertions.assertThrows(DataValidationException.class, () -> { + resourceService.save(resource); + }); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 7249ed6643..a4781bfcd6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -70,7 +70,7 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.transport.AbstractTransportIntegrationTest; import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import java.io.IOException; import java.net.ServerSocket; @@ -108,7 +108,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfil public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest { @SpyBean - DefaultLwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; + LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; @Autowired private LwM2mClientContext clientContextTest; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 7cb5f88733..449ac4c2ae 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -45,7 +45,7 @@ import org.junit.Assert; import org.mockito.Mockito; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import java.io.IOException; @@ -109,12 +109,12 @@ public class LwM2MTestClient { private LwM2MLocationParams locationParams; private LwM2mTemperatureSensor lwM2MTemperatureSensor; private Set clientStates; - private DefaultLwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; + private LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; private LwM2mClientContext clientContext; public void init(Security security, Configuration coapConfig, int port, boolean isRpc, boolean isBootstrap, int shortServerId, int shortServerIdBs, Security securityBs, - DefaultLwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, + LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mClientContext clientContext) throws InvalidDDFFileException, IOException { Assert.assertNull("client already initialized", leshanClient); this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java index 6cb6e49e79..6b84761208 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java @@ -16,7 +16,9 @@ package org.thingsboard.server.transport.mqtt; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttException; import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -36,9 +38,12 @@ import org.thingsboard.server.common.data.device.profile.JsonTransportPayloadCon import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.AbstractTransportIntegrationTest; +import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient; import java.util.List; @@ -103,6 +108,8 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg if (StringUtils.hasLength(config.getAttributesTopicFilter())) { mqttDeviceProfileTransportConfiguration.setDeviceAttributesTopic(config.getAttributesTopicFilter()); } + mqttDeviceProfileTransportConfiguration.setSparkplug(config.isSparkplug()); + mqttDeviceProfileTransportConfiguration.setSparkplugAttributesMetricNames(config.sparkplugAttributesMetricNames); mqttDeviceProfileTransportConfiguration.setSendAckOnValidationException(config.isSendAckOnValidationException()); TransportPayloadTypeConfiguration transportPayloadTypeConfiguration; if (TransportPayloadType.JSON.equals(transportPayloadType)) { @@ -176,4 +183,25 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg builder.addAllKv(kvProtos); return builder.build(); } + + protected void subscribeAndWait(MqttTestClient client, String attrSubTopic, DeviceId deviceId, FeatureType featureType) throws MqttException { + int subscriptionCount = getDeviceActorSubscriptionCount(deviceId, featureType); + client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); + // TODO: This test awaits for the device actor to receive the subscription. Ideally it should not happen. See details below: + // The transport layer acknowledge subscription request once the message about subscription is in the queue. + // Test sends data immediately after acknowledgement. + // But there is a time lag between push to the queue and read from the queue in the tb-core component. + // Ideally, we should reply to device with SUBACK only when the device actor on the tb-core receives the message. + awaitForDeviceActorToReceiveSubscription(deviceId, featureType, subscriptionCount + 1); + } + + protected void subscribeAndCheckSubscription(MqttTestClient client, String attrSubTopic, DeviceId deviceId, FeatureType featureType) throws MqttException { + client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); + // TODO: This test awaits for the device actor to receive the subscription. Ideally it should not happen. See details below: + // The transport layer acknowledge subscription request once the message about subscription is in the queue. + // Test sends data immediately after acknowledgement. + // But there is a time lag between push to the queue and read from the queue in the tb-core component. + // Ideally, we should reply to device with SUBACK only when the device actor on the tb-core receives the message. + awaitForDeviceActorToReceiveSubscription(deviceId, featureType, 1); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java index 8bedc48b73..5815adae76 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java @@ -20,12 +20,16 @@ import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.TransportPayloadType; +import java.util.Set; + @Data @Builder public class MqttTestConfigProperties { String deviceName; String gatewayName; + boolean isSparkplug; + Set sparkplugAttributesMetricNames; TransportPayloadType transportPayloadType; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestClient.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestClient.java index 511dabb5b7..e4b3058dfd 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestClient.java @@ -31,7 +31,7 @@ public class MqttTestClient { private static final String MQTT_URL = "tcp://localhost:1883"; private static final int TIMEOUT = 30; // seconds - private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(TIMEOUT); + public static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(TIMEOUT); private final MqttAsyncClient client; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java index 3a845509ef..b2fed98f2b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java @@ -32,12 +32,14 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportC import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.SingleEntityFilter; +import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; @@ -121,13 +123,16 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt // subscribe to attributes updates from server methods protected void processJsonTestSubscribeToAttributesUpdates(String attrSubTopic) throws Exception { + DeviceId deviceId = savedDevice.getId(); + MqttTestClient client = new MqttTestClient(); client.connectAndWait(accessToken); MqttTestCallback onUpdateCallback = new MqttTestCallback(); client.setCallback(onUpdateCallback); - client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + subscribeAndWait(client, attrSubTopic, deviceId, FeatureType.ATTRIBUTES); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + deviceId.getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); assertThat(onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) .as("await onUpdateCallback").isTrue(); @@ -135,7 +140,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt MqttTestCallback onDeleteCallback = new MqttTestCallback(); client.setCallback(onDeleteCallback); - doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class); + doDelete("/api/plugins/telemetry/DEVICE/" + deviceId.getId() + "/SHARED_SCOPE?keys=sharedJson", String.class); assertThat(onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) .as("await onDeleteCallback").isTrue(); validateUpdateAttributesJsonResponse(onDeleteCallback, SHARED_ATTRIBUTES_DELETED_RESPONSE); @@ -147,7 +152,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt client.connectAndWait(accessToken); MqttTestCallback onUpdateCallback = new MqttTestCallback(); client.setCallback(onUpdateCallback); - client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); + subscribeAndWait(client, attrSubTopic, savedDevice.getId(), FeatureType.ATTRIBUTES); doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); assertThat(onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) @@ -213,7 +218,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt assertNotNull(savedDevice); - client.subscribeAndWait(GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE); + subscribeAndCheckSubscription(client, GATEWAY_ATTRIBUTES_TOPIC, savedDevice.getId(), FeatureType.ATTRIBUTES); doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); assertThat(onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) @@ -244,7 +249,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt 20, 100); assertNotNull(device); - client.subscribeAndWait(GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE); + + subscribeAndCheckSubscription(client, GATEWAY_ATTRIBUTES_TOPIC, device.getId(), FeatureType.ATTRIBUTES); doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); validateProtoGatewayUpdateAttributesResponse(onUpdateCallback, deviceName); MqttTestCallback onDeleteCallback = new MqttTestCallback(); @@ -409,7 +415,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt Awaitility.await() .atMost(10, TimeUnit.SECONDS) .until(() -> { - List> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() {}); + List> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() { + }); return attributes.size() == 5; }); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java index 32f7647048..3880141c36 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java @@ -20,6 +20,7 @@ import org.eclipse.paho.client.mqttv3.MqttException; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.StringUtils; @@ -115,7 +116,7 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { testTelemetryIsDelivered(accessToken2Device, mqttTestClient5); } - @Test(expected = MqttException.class) + @Test public void testCorrectClientIdAndUserNameButWrongPassword() throws Exception { // Not correct. Correct clientId and username, but wrong password MqttTestClient mqttTestClient = new MqttTestClient(CLIENT_ID); @@ -125,7 +126,9 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { } catch (MqttException e) { Assert.assertEquals(4, e.getReasonCode()); // 4 - Reason code for bad username or password in MQTT v3 } - testTelemetryIsNotDelivered(clientIdAndUserNameAndPasswordDevice3, mqttTestClient); + Assertions.assertThrows(MqttException.class, () -> { + testTelemetryIsNotDelivered(clientIdAndUserNameAndPasswordDevice3, mqttTestClient); + }); } private void testTelemetryIsDelivered(Device device, MqttTestClient client) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java index 8610365af8..924b4d50a6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportC import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; +import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback; @@ -82,7 +83,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM client.connectAndWait(accessToken); MqttTestCallback callback = new MqttTestCallback(rpcSubTopic.replace("+", "0")); client.setCallback(callback); - client.subscribeAndWait(rpcSubTopic, MqttQoS.AT_MOST_ONCE); + subscribeAndWait(client, rpcSubTopic, savedDevice.getId(), FeatureType.RPC); String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}"; String result = doPostAsync("/api/rpc/oneway/" + savedDevice.getId(), setGpioRequest, String.class, status().isOk()); @@ -119,7 +120,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM protected void processJsonTwoWayRpcTest(String rpcSubTopic) throws Exception { MqttTestClient client = new MqttTestClient(); client.connectAndWait(accessToken); - client.subscribeAndWait(rpcSubTopic, MqttQoS.AT_LEAST_ONCE); + subscribeAndWait(client, rpcSubTopic, savedDevice.getId(), FeatureType.RPC); MqttTestRpcJsonCallback callback = new MqttTestRpcJsonCallback(client, rpcSubTopic.replace("+", "0")); client.setCallback(callback); String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; @@ -133,7 +134,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM protected void processProtoTwoWayRpcTest(String rpcSubTopic) throws Exception { MqttTestClient client = new MqttTestClient(); client.connectAndWait(accessToken); - client.subscribeAndWait(rpcSubTopic, MqttQoS.AT_LEAST_ONCE); + subscribeAndWait(client, rpcSubTopic, savedDevice.getId(), FeatureType.RPC); MqttTestRpcProtoCallback callback = new MqttTestRpcProtoCallback(client, rpcSubTopic.replace("+", "0")); client.setCallback(callback); @@ -194,7 +195,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM client.enableManualAcks(); MqttTestSequenceCallback callback = new MqttTestSequenceCallback(client, 10, result); client.setCallback(callback); - client.subscribeAndWait(DEVICE_RPC_REQUESTS_SUB_TOPIC, MqttQoS.AT_LEAST_ONCE); + subscribeAndWait(client, DEVICE_RPC_REQUESTS_SUB_TOPIC, savedDevice.getId(), FeatureType.RPC); callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertEquals(expected, result); @@ -223,6 +224,8 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM MqttTestCallback callback = new MqttTestCallback(GATEWAY_RPC_TOPIC); client.setCallback(callback); client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE); + subscribeAndCheckSubscription(client, GATEWAY_RPC_TOPIC, savedDevice.getId(), FeatureType.RPC); + String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; String deviceId = savedDevice.getId().getId().toString(); String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk()); @@ -269,7 +272,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM MqttTestRpcJsonCallback callback = new MqttTestRpcJsonCallback(client, GATEWAY_RPC_TOPIC); client.setCallback(callback); - client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE); + subscribeAndCheckSubscription(client, GATEWAY_RPC_TOPIC, savedDevice.getId(), FeatureType.RPC); String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; String deviceId = savedDevice.getId().getId().toString(); @@ -292,7 +295,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM MqttTestRpcProtoCallback callback = new MqttTestRpcProtoCallback(client, GATEWAY_RPC_TOPIC); client.setCallback(callback); - client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE); + subscribeAndCheckSubscription(client, GATEWAY_RPC_TOPIC, savedDevice.getId(), FeatureType.RPC); String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; String deviceId = savedDevice.getId().getId().toString(); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/MqttV5TestClient.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/MqttV5TestClient.java index b7ce8d10d3..76629b5a66 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/MqttV5TestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/MqttV5TestClient.java @@ -89,7 +89,9 @@ public class MqttV5TestClient { // We should copy part of MqttV3TestClient, due if (client == null) { throw new RuntimeException("Failed to connect! MqttAsyncClient is not initialized!"); } - return client.connect(options); + IMqttToken connect = client.connect(options); + connect.waitForCompletion(TIMEOUT_MS); + return connect; } public void disconnectAndWait() throws MqttException { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java new file mode 100644 index 0000000000..99134be5d4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java @@ -0,0 +1,462 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.sparkplug; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.mqttv5.client.IMqttToken; +import org.eclipse.paho.mqttv5.client.MqttCallback; +import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; +import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.packet.MqttConnAck; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; +import org.eclipse.paho.mqttv5.common.packet.MqttReturnCode; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; +import org.junit.Assert; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; +import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; +import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; +import org.thingsboard.server.transport.mqtt.mqttv5.MqttV5TestClient; +import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.awaitility.Awaitility.await; +import static org.eclipse.paho.mqttv5.common.packet.MqttWireMessage.MESSAGE_TYPE_CONNACK; +import static org.thingsboard.common.util.JacksonUtil.newArrayNode; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Bytes; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int16; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int32; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int64; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int8; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt16; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt64; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt8; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; + +/** + * Created by nickAS21 on 12.01.23 + */ +@Slf4j +public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttIntegrationTest { + + protected MqttV5TestClient client; + protected SparkplugMqttCallback mqttCallback; + protected Calendar calendar = Calendar.getInstance(); + protected ThreadLocalRandom random = ThreadLocalRandom.current(); + + protected static final String groupId = "SparkplugBGroupId"; + protected static final String edgeNode = "SparkpluBNode"; + protected static final String keysBdSeq = "bdSeq"; + protected static final String alias = "Failed Telemetry/Attribute proto sparkplug payload. SparkplugMessageType "; + protected String deviceId = "Test Sparkplug B Device"; + protected int bdSeq = 0; + protected int seq = 0; + protected static final long PUBLISH_TS_DELTA_MS = 86400000;// Publish start TS <-> 24h + + // NBIRTH + protected static final String keyNodeRebirth = "Node Control/Rebirth"; + + //*BIRTH + protected static final MetricDataType metricBirthDataType_Int32 = Int32; + protected static final String metricBirthName_Int32 = "Device Metric int32"; + protected Set sparkplugAttributesMetricNames; + + public void beforeSparkplugTest() throws Exception { + MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() + .gatewayName("Test Connect Sparkplug client node") + .isSparkplug(true) + .sparkplugAttributesMetricNames(sparkplugAttributesMetricNames) + .transportPayloadType(TransportPayloadType.PROTOBUF) + .build(); + processBeforeTest(configProperties); + } + + public void clientWithCorrectNodeAccessTokenWithNDEATH() throws Exception { + long ts = calendar.getTimeInMillis(); + long value = bdSeq = 0; + clientWithCorrectNodeAccessTokenWithNDEATH(ts, value); + } + + public void clientWithCorrectNodeAccessTokenWithNDEATH(long ts, long value) throws Exception { + IMqttToken connectionResult = clientConnectWithNDEATH(ts, value); + MqttWireMessage response = connectionResult.getResponse(); + Assert.assertEquals(MESSAGE_TYPE_CONNACK, response.getType()); + MqttConnAck connAckMsg = (MqttConnAck) response; + Assert.assertEquals(MqttReturnCode.RETURN_CODE_SUCCESS, connAckMsg.getReturnCode()); + } + + public IMqttToken clientConnectWithNDEATH(long ts, long value, String... nameSpaceBad) throws Exception { + String key = keysBdSeq; + MetricDataType metricDataType = Int64; + SparkplugBProto.Payload.Builder deathPayload = SparkplugBProto.Payload.newBuilder() + .setTimestamp(calendar.getTimeInMillis()); + deathPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + byte[] deathBytes = deathPayload.build().toByteArray(); + this.client = new MqttV5TestClient(); + this.mqttCallback = new SparkplugMqttCallback(); + this.client.setCallback(this.mqttCallback); + MqttConnectionOptions options = new MqttConnectionOptions(); + options.setUserName(gatewayAccessToken); + String nameSpace = nameSpaceBad.length == 0 ? NAMESPACE : nameSpaceBad[0]; + String topic = nameSpace + "/" + groupId + "/" + SparkplugMessageType.NDEATH.name() + "/" + edgeNode; + MqttMessage msg = new MqttMessage(); + msg.setId(0); + msg.setPayload(deathBytes); + options.setWill(topic, msg); + return client.connect(options); + } + + protected List connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(int cntDevices, long ts) throws Exception { + List devices = new ArrayList<>(); + clientWithCorrectNodeAccessTokenWithNDEATH(); + MetricDataType metricDataType = Int32; + String key = "Node Metric int32"; + int valueDeviceInt32 = 1024; + SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType); + SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder() + .setTimestamp(ts) + .setSeq(getBdSeqNum()); + payloadBirthNode.addMetrics(metric); + payloadBirthNode.setTimestamp(ts); + if (client.isConnected()) { + client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, + payloadBirthNode.build().toByteArray(), 0, false); + } + + valueDeviceInt32 = 4024; + metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32); + for (int i = 0; i < cntDevices; i++) { + SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder() + .setTimestamp(ts) + .setSeq(getSeqNum()); + String deviceName = deviceId + "_" + i; + + payloadBirthDevice.addMetrics(metric); + if (client.isConnected()) { + client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName, + payloadBirthDevice.build().toByteArray(), 0, false); + AtomicReference device = new AtomicReference<>(); + await(alias + "find device [" + deviceName + "] after created") + .atMost(200, TimeUnit.SECONDS) + .until(() -> { + device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class)); + return device.get() != null; + }); + devices.add(device.get()); + } + + } + + Assert.assertEquals(cntDevices, devices.size()); + return devices; + } + + protected long getBdSeqNum() throws Exception { + if (bdSeq == 256) { + bdSeq = 0; + } + return bdSeq++; + } + + protected long getSeqNum() throws Exception { + if (seq == 256) { + seq = 0; + } + return seq++; + } + + protected List connectionWithNBirth(MetricDataType metricDataType, String metricKey, Object metricValue) throws Exception { + List listKeys = new ArrayList<>(); + SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder() + .setTimestamp(calendar.getTimeInMillis()); + long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + long valueBdSec = getBdSeqNum(); + payloadBirthNode.addMetrics(createMetric(valueBdSec, ts, keysBdSeq, Int64)); + listKeys.add(SparkplugMessageType.NBIRTH.name() + " " + keysBdSeq); + payloadBirthNode.addMetrics(createMetric(false, ts, keyNodeRebirth, MetricDataType.Boolean)); + listKeys.add(keyNodeRebirth); + + payloadBirthNode.addMetrics(createMetric(metricValue, ts, metricKey, metricDataType)); + listKeys.add(metricKey); + + if (client.isConnected()) { + client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, + payloadBirthNode.build().toByteArray(), 0, false); + } + return listKeys; + } + + protected void createdAddMetricValuePrimitiveTsKv(List listTsKvEntry, List listKeys, + SparkplugBProto.Payload.Builder dataPayload, long ts) throws ThingsboardException { + + String keys = "MyInt8"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt8(), ts, Int8)); + listKeys.add(keys); + + keys = "MyInt16"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt16(), ts, Int16)); + listKeys.add(keys); + + keys = "MyInt32"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt32(), ts, Int32)); + listKeys.add(keys); + + keys = "MyInt64"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt64(), ts, Int64)); + listKeys.add(keys); + + keys = "MyUInt8"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt8(), ts, UInt8)); + listKeys.add(keys); + + keys = "MyUInt16"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt16(), ts, UInt16)); + listKeys.add(keys); + + keys = "MyUInt32"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt32(), ts, UInt32)); + listKeys.add(keys); + + keys = "MyUInt64"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt64(), ts, UInt64)); + listKeys.add(keys); + + keys = "MyFloat"; + listTsKvEntry.add(createdAddMetricTsKvFloat(dataPayload, keys, nextFloat(0, 100), ts, MetricDataType.Float)); + listKeys.add(keys); + + keys = "MyDateTime"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextDateTime(), ts, MetricDataType.DateTime)); + listKeys.add(keys); + + keys = "MyDouble"; + listTsKvEntry.add(createdAddMetricTsKvDouble(dataPayload, keys, nextDouble(), ts, MetricDataType.Double)); + listKeys.add(keys); + + keys = "MyBoolean"; + listTsKvEntry.add(createdAddMetricTsKvBoolean(dataPayload, keys, nextBoolean(), ts, MetricDataType.Boolean)); + listKeys.add(keys); + + keys = "MyString"; + listTsKvEntry.add(createdAddMetricTsKvString(dataPayload, keys, nextString(), ts, MetricDataType.String)); + listKeys.add(keys); + + keys = "MyText"; + listTsKvEntry.add(createdAddMetricTsKvString(dataPayload, keys, nextString(), ts, MetricDataType.Text)); + listKeys.add(keys); + + keys = "MyUUID"; + listTsKvEntry.add(createdAddMetricTsKvString(dataPayload, keys, nextString(), ts, MetricDataType.UUID)); + listKeys.add(keys); + + } + + protected void createdAddMetricValueArraysPrimitiveTsKv(List listTsKvEntry, List listKeys, + SparkplugBProto.Payload.Builder dataPayload, long ts) throws ThingsboardException { + String keys = "MyBytesArray"; + byte[] bytes = {nextInt8(), nextInt8(), nextInt8()}; + createdAddMetricTsKvJson(dataPayload, keys, bytes, ts, Bytes, listTsKvEntry, listKeys); + } + + private TsKvEntry createdAddMetricTsKvLong(SparkplugBProto.Payload.Builder dataPayload, String key, Object value, + long ts, MetricDataType metricDataType) throws ThingsboardException { + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, Long.valueOf(String.valueOf(value)))); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + return tsKvEntry; + } + + private TsKvEntry createdAddMetricTsKvFloat(SparkplugBProto.Payload.Builder dataPayload, String key, float value, + long ts, MetricDataType metricDataType) throws ThingsboardException { + Double dd = Double.parseDouble(Float.toString(value)); + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new DoubleDataEntry(key, dd)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + return tsKvEntry; + } + + private TsKvEntry createdAddMetricTsKvDouble(SparkplugBProto.Payload.Builder dataPayload, String key, double value, + long ts, MetricDataType metricDataType) throws ThingsboardException { + Long l = Double.valueOf(value).longValue(); + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, l)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + return tsKvEntry; + } + + private TsKvEntry createdAddMetricTsKvBoolean(SparkplugBProto.Payload.Builder dataPayload, String key, boolean value, + long ts, MetricDataType metricDataType) throws ThingsboardException { + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new BooleanDataEntry(key, value)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + return tsKvEntry; + } + + private TsKvEntry createdAddMetricTsKvString(SparkplugBProto.Payload.Builder dataPayload, String key, String value, + long ts, MetricDataType metricDataType) throws ThingsboardException { + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(key, value)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + return tsKvEntry; + } + + private void createdAddMetricTsKvJson(SparkplugBProto.Payload.Builder dataPayload, String key, + Object values, long ts, MetricDataType metricDataType, + List listTsKvEntry, + List listKeys) throws ThingsboardException { + ArrayNode nodeArray = newArrayNode(); + switch (metricDataType) { + case Bytes: + for (byte b : (byte[]) values) { + nodeArray.add(b); + } + break; + default: + throw new IllegalStateException("Unexpected value: " + metricDataType); + } + if (nodeArray.size() > 0) { + Optional tsKvEntryOptional = Optional.of(new BasicTsKvEntry(ts, new JsonDataEntry(key, nodeArray.toString()))); + if (tsKvEntryOptional.isPresent()) { + dataPayload.addMetrics(createMetric(values, ts, key, metricDataType)); + listTsKvEntry.add(tsKvEntryOptional.get()); + listKeys.add(key); + } + } + } + + private byte nextInt8() { + return (byte) random.nextInt(Byte.MIN_VALUE, Byte.MAX_VALUE); + } + + private short nextUInt8() { + return (short) random.nextInt(0, Byte.MAX_VALUE * 2 + 1); + } + + private short nextInt16() { + return (short) random.nextInt(Short.MIN_VALUE, Short.MAX_VALUE); + } + + private int nextUInt16() { + return random.nextInt(0, Short.MAX_VALUE * 2 + 1); + } + + protected int nextInt32() { + return random.nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + protected long nextUInt32() { + long l = Integer.MAX_VALUE; + return random.nextLong(0, l * 2 + 1); + } + + private long nextInt64() { + return random.nextLong(Long.MIN_VALUE, Long.MAX_VALUE); + } + + private long nextUInt64() { + double d = Long.MAX_VALUE; + return random.nextLong(0, (long) (d * 2 + 1)); + } + + protected double nextDouble() { + return random.nextDouble(Long.MIN_VALUE, Long.MAX_VALUE); + } + + private long nextDateTime() { + long min = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + long max = calendar.getTimeInMillis(); + return random.nextLong(min, max); + } + + protected float nextFloat(float min, float max) { + if (min >= max) + throw new IllegalArgumentException("max must be greater than min"); + float result = ThreadLocalRandom.current().nextFloat() * (max - min) + min; + if (result >= max) // correct for rounding + result = Float.intBitsToFloat(Float.floatToIntBits(max) - 1); + return result; + } + + protected boolean nextBoolean() { + return random.nextBoolean(); + } + + protected String nextString() { + return java.util.UUID.randomUUID().toString(); + } + + public class SparkplugMqttCallback implements MqttCallback { + private final List messageArrivedMetrics = new ArrayList<>(); + + @Override + public void disconnected(MqttDisconnectResponse mqttDisconnectResponse) { + + } + + @Override + public void mqttErrorOccurred(MqttException e) { + + } + + @Override + public void messageArrived(String topic, MqttMessage mqttMsg) throws Exception { + SparkplugBProto.Payload sparkplugBProtoNode = SparkplugBProto.Payload.parseFrom(mqttMsg.getPayload()); + messageArrivedMetrics.addAll(sparkplugBProtoNode.getMetricsList()); + } + + @Override + public void deliveryComplete(IMqttToken iMqttToken) { + + } + + @Override + public void connectComplete(boolean b, String s) { + + } + + @Override + public void authPacketArrived(int i, MqttProperties mqttProperties) { + + } + + public List getMessageArrivedMetrics() { + return messageArrivedMetrics; + } + + public void deleteMessageArrivedMetrics(int id) { + messageArrivedMetrics.remove(id); + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java new file mode 100644 index 0000000000..04349d0672 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java @@ -0,0 +1,432 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.sparkplug.attributes; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest; +import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; + +/** + * Created by nickAS21 on 12.01.23 + */ +@Slf4j +public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends AbstractMqttV5ClientSparkplugTest { + + protected void processClientWithCorrectAccessTokenPublishNCMDReBirth() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + List listKeys = connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); + // Shared attribute "Node Control/Rebirth" = true. type = NCMD. + boolean value = true; + Assert.assertTrue(listKeys.contains(keyNodeRebirth)); + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + keyNodeRebirth + "\":" + value + "}"; + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(keyNodeRebirth, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); + } + + /** + * If boolean - send long 0 or 1 + * If String - try to parse long + * If double - cast long + * If we can't parse, cast, or JSON there - debug the message with the id of the devise/node, tenant, + * the name and type of the attribute into an error and don't send anything. + */ + protected void processClientWithCorrectAccessTokenPublishNCMD_BooleanType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + MetricDataType metricDataType = MetricDataType.Boolean; + String metricKey = "MyBoolean"; + Object metricValue = nextBoolean(); + connectionWithNBirth(metricDataType, metricKey, metricValue); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + + // Boolean <-> String + boolean expectedValue = true; + String valueStr = "123"; + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + expectedValue = false; + valueStr = "0"; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // Boolean <-> Integer + expectedValue = true; + Integer valueInt = nextInt32(); + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueInt + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + expectedValue = false; + valueInt = 0; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueInt + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); + } + + protected void processClientWithCorrectAccessTokenPublishNCMD_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + MetricDataType metricDataType = UInt32; + String metricKey = "MyLong"; + Object metricValue = nextUInt32(); + connectionWithNBirth(metricDataType, metricKey, metricValue); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + + // Long <-> String + String valueStr = "123"; + long expectedValue = Long.valueOf(valueStr); + + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getLongValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // Long <-> Boolean + Boolean valueBoolean = true; + expectedValue = 1L; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getLongValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + valueBoolean = false; + expectedValue = 0L; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getLongValue()); + } + + protected void processClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + MetricDataType metricDataType = MetricDataType.Float; + String metricKey = "MyFloat"; + Object metricValue = nextFloat(30, 400); + connectionWithNBirth(metricDataType, metricKey, metricValue); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + + // Float <-> String + String valueStr = "123.345"; + float expectedValue = Float.valueOf(valueStr); + + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // Float <-> Boolean + Boolean valueBoolean = true; + expectedValue = 1f; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + valueBoolean = false; + expectedValue = 0f; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue()); + } + + protected void processClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + MetricDataType metricDataType = MetricDataType.Double; + String metricKey = "MyDouble"; + Object metricValue = nextDouble(); + connectionWithNBirth(metricDataType, metricKey, metricValue); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + + // Double <-> String + String valueStr = "123345456"; + double expectedValue = Double.valueOf(valueStr); + + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getDoubleValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // Double <-> Boolean + Boolean valueBoolean = true; + expectedValue = 1d; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getDoubleValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + valueBoolean = false; + expectedValue = 0d; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getDoubleValue()); + } + + protected void processClientWithCorrectAccessTokenPublishNCMD_StringType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + MetricDataType metricDataType = MetricDataType.String; + String metricKey = "MyString"; + Object metricValue = nextString(); + connectionWithNBirth(metricDataType, metricKey, metricValue); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + + // String <-> Long + long valueLong = 123345456L; + String expectedValue = String.valueOf(valueLong); + + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueLong + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getStringValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // String <-> Boolean + Boolean valueBoolean = true; + expectedValue = "true"; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getStringValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + valueBoolean = false; + expectedValue = "false"; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getStringValue()); + } + + protected void processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttribute() throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); + + // Integer <-> Integer + int expectedValueInt = 123456; + + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + expectedValueInt + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.DBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValueInt, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + } + + protected void processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); + + // Int <-> String + String valueStr = "123"; + long expectedValue = Long.valueOf(valueStr); + + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + valueStr + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.DBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // Int <-> Boolean + Boolean valueBoolean = true; + expectedValue = 1; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + valueBoolean = false; + expectedValue = 0; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + } + + protected void processClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); + String urlTemplate = "/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/keys/attributes/" + CLIENT_SCOPE; + AtomicReference> actualKeys = new AtomicReference<>(); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() { + })); + return actualKeys.get().size() == 1; + }); + Assert.assertEquals(metricBirthName_Int32, actualKeys.get().get(0)); + } + + protected void processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); + String urlTemplate = "/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/keys/attributes/" + CLIENT_SCOPE; + AtomicReference> actualKeys = new AtomicReference<>(); + await(alias + SparkplugMessageType.DBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() { + })); + return actualKeys.get().size() == 1; + }); + Assert.assertEquals(metricBirthName_Int32, actualKeys.get().get(0)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java new file mode 100644 index 0000000000..1d5a2c127f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.sparkplug.attributes; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.HashSet; + +/** + * Created by nickAS21 on 12.01.23 + */ +@DaoSqlTest +public class MqttV5ClientSparkplugBAttributesInProfileTest extends AbstractMqttV5ClientSparkplugAttributesTest { + + @Before + public void beforeTest() throws Exception { + sparkplugAttributesMetricNames = new HashSet<>(); + sparkplugAttributesMetricNames.add(metricBirthName_Int32); + beforeSparkplugTest(); + } + + @After + public void afterTest () throws MqttException { + if (client.isConnected()) { + client.disconnect(); } + } + + @Test + public void testClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { + processClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes(); + } + + @Test + public void testClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { + processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes(); + } + +} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java new file mode 100644 index 0000000000..f5d42046b7 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java @@ -0,0 +1,81 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.sparkplug.attributes; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.dao.service.DaoSqlTest; + +/** + * Created by nickAS21 on 12.01.23 + */ +@DaoSqlTest +public class MqttV5ClientSparkplugBAttributesTest extends AbstractMqttV5ClientSparkplugAttributesTest { + + @Before + public void beforeTest() throws Exception { + beforeSparkplugTest(); + } + + @After + public void afterTest () throws MqttException { + if (client.isConnected()) { + client.disconnect(); } + } + + @Test + public void testClientWithCorrectAccessTokenPublishNCMDReBirth() throws Exception { + processClientWithCorrectAccessTokenPublishNCMDReBirth(); + } + + @Test + public void testClientWithCorrectAccessTokenPublishNCMD_BooleanType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + processClientWithCorrectAccessTokenPublishNCMD_BooleanType_IfMetricFailedTypeCheck_SendValueOk(); + } + + @Test + public void testClientWithCorrectAccessTokenPublishNCMD_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + processClientWithCorrectAccessTokenPublishNCMD_LongType_IfMetricFailedTypeCheck_SendValueOk(); + } + + @Test + public void testClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + processClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk(); + } + + @Test + public void testClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + processClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk(); + } + + @Test + public void testClientWithCorrectAccessTokenPublishNCMD_StringType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + processClientWithCorrectAccessTokenPublishNCMD_StringType_IfMetricFailedTypeCheck_SendValueOk(); + } + + @Test + public void testClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttribute() throws Exception { + processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttribute(); + } + + @Test + public void testClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk(); + } + +} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java new file mode 100644 index 0000000000..5f9b794739 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java @@ -0,0 +1,178 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.sparkplug.connection; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.junit.Assert; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; +import org.thingsboard.server.transport.mqtt.mqttv5.MqttV5TestClient; +import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.awaitility.Awaitility.await; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.OFFLINE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; + +/** + * Created by nickAS21 on 12.01.23 + */ +@Slf4j +public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends AbstractMqttV5ClientSparkplugTest { + + protected void processClientWithCorrectNodeAccessTokenWithNDEATH_Test() throws Exception { + long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + long value = bdSeq = 0; + clientWithCorrectNodeAccessTokenWithNDEATH(ts, value); + + String keys = SparkplugMessageType.NDEATH.name() + " " + keysBdSeq; + TsKvEntry expectedTsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(keys, value)); + AtomicReference>> finalFuture = new AtomicReference<>(); + await(alias + SparkplugMessageType.NDEATH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findLatest(tenantId, savedGateway.getId(), keys)); + return finalFuture.get().get().isPresent(); + }); + TsKvEntry actualTsKvEntry = finalFuture.get().get().get(); + Assert.assertEquals(expectedTsKvEntry, actualTsKvEntry); + } + + protected void processClientWithCorrectNodeAccessTokenWithoutNDEATH_Test() throws Exception { + this.client = new MqttV5TestClient(); + MqttException actualException = Assert.assertThrows(MqttException.class, () -> client.connectAndWait(gatewayAccessToken)); + String expectedMessage = "Server unavailable."; + int expectedReasonCode = 136; + Assert.assertEquals(expectedMessage, actualException.getMessage()); + Assert.assertEquals(expectedReasonCode, actualException.getReasonCode()); + } + + protected void processClientWithCorrectNodeAccessTokenNameSpaceInvalid_Test() throws Exception { + long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + long value = bdSeq = 0; + MqttException actualException = Assert.assertThrows(MqttException.class, () -> clientConnectWithNDEATH(ts, value, "spBv1.2")); + String expectedMessage = "Server unavailable."; + int expectedReasonCode = 136; + Assert.assertEquals(expectedMessage, actualException.getMessage()); + Assert.assertEquals(expectedReasonCode, actualException.getReasonCode()); + } + + protected void processClientWithCorrectAccessTokenWithNDEATHCreatedDevices(int cntDevices) throws Exception { + long ts = calendar.getTimeInMillis(); + connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); + } + + protected void processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_ALL(int cntDevices) throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); + + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), ONLINE.name())); + AtomicReference>> finalFuture = new AtomicReference<>(); + await(alias + messageName(STATE) + ", device: " + savedGateway.getName()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); + return finalFuture.get().get().contains(tsKvEntry); + }); + + for (Device device : devices) { + await(alias + messageName(STATE) + ", device: " + device.getName()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, device.getId())); + return finalFuture.get().get().contains(tsKvEntry); + }); + } + } + + protected void processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OneDeviceOFFLINE(int cntDevices, int indexDeviceDisconnect) throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); + + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), OFFLINE.name())); + AtomicReference>> finalFuture = new AtomicReference<>(); + + SparkplugBProto.Payload.Builder payloadDeathDevice = SparkplugBProto.Payload.newBuilder() + .setTimestamp(ts) + .setSeq(getSeqNum()); + if (client.isConnected()) { + List devicesList = new ArrayList<>(devices); + Device device = devicesList.get(indexDeviceDisconnect); + client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.DDEATH.name() + "/" + edgeNode + "/" + device.getName(), + payloadDeathDevice.build().toByteArray(), 0, false); + await(alias + messageName(STATE) + ", device: " + device.getName()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, device.getId())); + return findEqualsKeyValueInKvEntrys(finalFuture.get().get(), tsKvEntry); + }); + } + } + + protected void processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OFFLINE_All(int cntDevices) throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); + + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), OFFLINE.name())); + AtomicReference>> finalFuture = new AtomicReference<>(); + + if (client.isConnected()) { + client.disconnect(); + + await(alias + messageName(STATE) + ", device: " + savedGateway.getName()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); + return findEqualsKeyValueInKvEntrys(finalFuture.get().get(), tsKvEntry); + }); + + List devicesList = new ArrayList<>(devices); + for (Device device : devicesList) { + await(alias + messageName(STATE) + ", device: " + device.getName()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, device.getId())); + return findEqualsKeyValueInKvEntrys(finalFuture.get().get(), tsKvEntry); + }); + } + } + } + + private boolean findEqualsKeyValueInKvEntrys(List finalFuture, TsKvEntry tsKvEntry) { + for (TsKvEntry kvEntry : finalFuture) { + if (kvEntry.getKey().equals(tsKvEntry.getKey()) && kvEntry.getValue().equals(tsKvEntry.getValue())) { + return true; + } + } + return false; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionTest.java new file mode 100644 index 0000000000..71c9391a29 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionTest.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.sparkplug.connection; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.dao.service.DaoSqlTest; + +/** + * Created by nickAS21 on 12.01.23 + */ +@DaoSqlTest +public class MqttV5ClientSparkplugBConnectionTest extends AbstractMqttV5ClientSparkplugConnectionTest { + + @Before + public void beforeTest() throws Exception { + beforeSparkplugTest(); + } + + @After + public void afterTest() throws MqttException { + if (client.isConnected()) { + client.disconnect(); + } + } + + @Test + public void testClientWithCorrectAccessTokenWithNDEATH() throws Exception { + processClientWithCorrectNodeAccessTokenWithNDEATH_Test(); + } + + @Test + public void testClientWithCorrectNodeAccessTokenWithoutNDEATH() throws Exception { + processClientWithCorrectNodeAccessTokenWithoutNDEATH_Test(); + } + + @Test + public void testClientWithCorrectNodeAccessTokenNameSpaceInvalid() throws Exception { + processClientWithCorrectNodeAccessTokenNameSpaceInvalid_Test(); + } + + @Test + public void testClientWithCorrectAccessTokenWithNDEATHCreatedOneDevice() throws Exception { + processClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1); + } + + @Test + public void testClientWithCorrectAccessTokenWithNDEATHCreatedTwoDevice() throws Exception { + processClientWithCorrectAccessTokenWithNDEATHCreatedDevices(2); + } + + @Test + public void testClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_ALL() throws Exception { + processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_ALL(3); + } + + @Test + public void testConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OneDeviceOFFLINE() throws Exception { + processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OneDeviceOFFLINE(3, 1); + } + + @Test + public void testConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OFFLINE_All() throws Exception { + processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OFFLINE_All(3); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java new file mode 100644 index 0000000000..b08baac875 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java @@ -0,0 +1,108 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.sparkplug.rpc; + +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.exception.ThingsboardErrorCode.INVALID_ARGUMENTS; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DCMD; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; + +@Slf4j +public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5ClientSparkplugTest { + + private static final int metricBirthValue_Int32 = 123456; + private static final String sparkplugRpcRequest = "{\"metricName\":\"" + metricBirthName_Int32 + "\",\"value\":" + metricBirthValue_Int32 + "}"; + + @Test + public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + String expected = "{\"result\":\"Success: " + SparkplugMessageType.NCMD.name() + "\"}"; + String actual = sendRPCSparkplug(NCMD.name(), sparkplugRpcRequest, savedGateway); + await(alias + SparkplugMessageType.NCMD.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(expected, actual); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + } + + @Test + public void processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); + String expected = "{\"result\":\"Success: " + DCMD.name() + "\"}"; + String actual = sendRPCSparkplug(DCMD.name() , sparkplugRpcRequest, devices.get(0)); + await(alias + NCMD.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(expected, actual); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + } + + @Test + public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + String invalidateTypeMessageName = "RCMD"; + String expected = "{\"result\":\"" + INVALID_ARGUMENTS + "\",\"error\":\"Failed to convert device RPC command to MQTT msg: " + + invalidateTypeMessageName + "{\\\"metricName\\\":\\\"" + metricBirthName_Int32 + "\\\",\\\"value\\\":" + metricBirthValue_Int32 + "}\"}"; + String actual = sendRPCSparkplug(invalidateTypeMessageName, sparkplugRpcRequest, savedGateway); + Assert.assertEquals(expected, actual); + } + + @Test + public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InBirthNotHaveMetric_BAD_REQUEST_PARAMS() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + String metricNameBad = metricBirthName_Int32 + "_Bad"; + String sparkplugRpcRequestBad = "{\"metricName\":\"" + metricNameBad + "\",\"value\":" + metricBirthValue_Int32 + "}"; + String expected = "{\"result\":\"BAD_REQUEST_PARAMS\",\"error\":\"Failed send To Node Rpc Request: " + + DCMD.name() + ". This node does not have a metricName: [" + metricNameBad + "]\"}"; + String actual = sendRPCSparkplug(DCMD.name(), sparkplugRpcRequestBad, savedGateway); + Assert.assertEquals(expected, actual); + } + + private String sendRPCSparkplug(String nameTypeMessage, String keyValue, Device device) throws Exception { + String setRpcRequest = "{\"method\": \"" + nameTypeMessage + "\", \"params\": " + keyValue + "}"; + return doPostAsync("/api/plugins/rpc/twoway/" + device.getId().getId(), setRpcRequest, String.class, status().isOk()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java new file mode 100644 index 0000000000..9f568b550f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.sparkplug.rpc; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +@Slf4j +public class MqttV5RpcSparkplugTest extends AbstractMqttV5RpcSparkplugTest { + + @Before + public void beforeTest() throws Exception { + beforeSparkplugTest(); + } + + @After + public void afterTest() throws MqttException { + if (client.isConnected()) { + client.disconnect(); + } + } + + @Test + public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { + processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_Success(); + } + + @Test + public void testClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { + processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success(); + } + + @Test + public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception { + processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS(); + } + + @Test + public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InBirthNotHaveMetric_BAD_REQUEST_PARAMS() throws Exception { + processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java new file mode 100644 index 0000000000..587494f869 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java @@ -0,0 +1,113 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.sparkplug.timeseries; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; +import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.awaitility.Awaitility.await; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; + +/** + * Created by nickAS21 on 12.01.23 + */ +@Slf4j +public abstract class AbstractMqttV5ClientSparkplugTelemetryTest extends AbstractMqttV5ClientSparkplugTest { + + protected void processClientWithCorrectAccessTokenPublishNBIRTH() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + List listKeys = connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); + Assert.assertTrue("Connection node is failed", client.isConnected()); + AtomicReference>> finalFuture = new AtomicReference<>(); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findLatest(tenantId, savedGateway.getId(), listKeys)); + return !finalFuture.get().get().isEmpty(); + }); + Assert.assertEquals(listKeys.size(), finalFuture.get().get().size()); + } + + protected void processClientWithCorrectAccessTokenPushNodeMetricBuildPrimitiveSimple() throws Exception { + List listKeys = new ArrayList<>(); + clientWithCorrectNodeAccessTokenWithNDEATH(); + + String messageTypeName = SparkplugMessageType.NDATA.name(); + + List listTsKvEntry = new ArrayList<>(); + + SparkplugBProto.Payload.Builder ndataPayload = SparkplugBProto.Payload.newBuilder() + .setTimestamp(calendar.getTimeInMillis()) + .setSeq(getSeqNum()); + long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + + createdAddMetricValuePrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts); + + if (client.isConnected()) { + client.publish(NAMESPACE + "/" + groupId + "/" + messageTypeName + "/" + edgeNode, + ndataPayload.build().toByteArray(), 0, false); + } + + AtomicReference>> finalFuture = new AtomicReference<>(); + await(alias + SparkplugMessageType.NDATA.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); + return finalFuture.get().get().size() == (listTsKvEntry.size() + 1); + }); + Assert.assertTrue("Actual tsKvEntrys is not containsAll Expected tsKvEntrys", finalFuture.get().get().containsAll(listTsKvEntry)); + } + + protected void processClientWithCorrectAccessTokenPushNodeMetricBuildArraysPrimitiveSimple() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + + String messageTypeName = SparkplugMessageType.NDATA.name(); + List listKeys = new ArrayList<>(); + List listTsKvEntry = new ArrayList<>(); + + SparkplugBProto.Payload.Builder ndataPayload = SparkplugBProto.Payload.newBuilder() + .setTimestamp(calendar.getTimeInMillis()) + .setSeq(getSeqNum()); + long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + + createdAddMetricValueArraysPrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts); + + if (client.isConnected()) { + client.publish(NAMESPACE + "/" + groupId + "/" + messageTypeName + "/" + edgeNode, + ndataPayload.build().toByteArray(), 0, false); + } + + AtomicReference>> finalFuture = new AtomicReference<>(); + await(alias + SparkplugMessageType.NDATA.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); + return finalFuture.get().get().size() == (listTsKvEntry.size() + 1); + }); + Assert.assertTrue("Actual tsKvEntrys is not containsAll Expected tsKvEntrys", finalFuture.get().get().containsAll(listTsKvEntry)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/MqttV5ClientSparkplugBTelemetryTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/MqttV5ClientSparkplugBTelemetryTest.java new file mode 100644 index 0000000000..bdd6c7b9b3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/MqttV5ClientSparkplugBTelemetryTest.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.sparkplug.timeseries; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.dao.service.DaoSqlTest; + +/** + * Created by nickAS21 on 12.01.23 + */ +@DaoSqlTest +public class MqttV5ClientSparkplugBTelemetryTest extends AbstractMqttV5ClientSparkplugTelemetryTest { + + @Before + public void beforeTest() throws Exception { + beforeSparkplugTest(); + } + + @After + public void afterTest () throws MqttException { + if (client.isConnected()) { + client.disconnect(); } + } + + @Test + public void testClientWithCorrectAccessTokenPublishNBIRTH() throws Exception { + processClientWithCorrectAccessTokenPublishNBIRTH(); + } + + @Test + public void testClientWithCorrectAccessTokenPushNodeMetricBuildPrimitiveSimple() throws Exception { + processClientWithCorrectAccessTokenPushNodeMetricBuildPrimitiveSimple(); + } + + @Test + public void testClientWithCorrectAccessTokenPushNodeMetricBuildPArraysPrimitiveSimple() throws Exception { + processClientWithCorrectAccessTokenPushNodeMetricBuildArraysPrimitiveSimple(); + } + +} \ No newline at end of file diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 23e6d8c2f6..953b7094a4 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -21,6 +21,13 @@ + + + + + + + diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 93e1327050..c1e6f4d1eb 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -186,6 +186,7 @@ message GetOrCreateDeviceFromGatewayRequestMsg { int64 gatewayIdLSB = 2; string deviceName = 3; string deviceType = 4; + bool sparkplug = 5; } message GetOrCreateDeviceFromGatewayResponseMsg { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java index 9b944d7902..37ef1b65ac 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java @@ -16,7 +16,6 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; -import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; @Data diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java index ae36567ae7..d0b83e56d6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java @@ -19,6 +19,8 @@ import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.validation.NoXss; +import java.util.Set; + @Data public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { @@ -27,6 +29,8 @@ public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTra @NoXss private String deviceAttributesTopic = MqttTopics.DEVICE_ATTRIBUTES_TOPIC; private TransportPayloadTypeConfiguration transportPayloadTypeConfiguration; + private boolean sparkplug; + private Set sparkplugAttributesMetricNames; private boolean sendAckOnValidationException; @Override diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java index e2af72892f..cad0d9370c 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data; import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Assert; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @@ -58,9 +59,11 @@ public class UUIDConverterTest { Assert.assertEquals(UUID.fromString("58e0a7d7-eebc-11d8-9669-0800200c9a66"), result); } - @Test(expected = IllegalArgumentException.class) + @Test public void nonV1UuidToStringTest() { - UUIDConverter.fromTimeUUID(UUID.fromString("58e0a7d7-eebc-01d8-9669-0800200c9a66")); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + UUIDConverter.fromTimeUUID(UUID.fromString("58e0a7d7-eebc-01d8-9669-0800200c9a66")); + }); } @Test diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index f4b52a0f5f..3eb096cd38 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -25,6 +25,7 @@ import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; import org.eclipse.leshan.server.californium.LeshanServer; import org.eclipse.leshan.server.californium.LeshanServerBuilder; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; +import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.ota.OtaPackageDataCache; import org.thingsboard.server.common.data.DataConstants; @@ -35,7 +36,7 @@ import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer; import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MDtlsCertificateVerifier; import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import javax.annotation.PreDestroy; @@ -56,6 +57,7 @@ import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaU @Slf4j @Component +@DependsOn({"lwM2mDownlinkMsgHandler", "lwM2mUplinkMsgHandler"}) @TbLwM2mTransportComponent @RequiredArgsConstructor public class DefaultLwM2mTransportService implements LwM2MTransportService { @@ -66,7 +68,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { private final LwM2mTransportContext context; private final LwM2MTransportServerConfig config; private final OtaPackageDataCache otaPackageDataCache; - private final DefaultLwM2mUplinkMsgHandler handler; + private final LwM2mUplinkMsgHandler handler; private final CaliforniumRegistrationStore registrationStore; private final TbSecurityStore securityStore; private final TbLwM2MDtlsCertificateVerifier certificateVerifier; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 49a77d1eea..d624badc1a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -42,7 +42,7 @@ import org.thingsboard.server.transport.lwm2m.server.ota.LwM2MOtaUpdateService; import org.thingsboard.server.transport.lwm2m.server.session.LwM2MSessionManager; import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MClientStore; import org.thingsboard.server.transport.lwm2m.server.store.TbMainSecurityStore; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil; import java.util.Arrays; @@ -75,7 +75,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @Autowired @Lazy - private DefaultLwM2mUplinkMsgHandler defaultLwM2MUplinkMsgHandler; + private LwM2mUplinkMsgHandler defaultLwM2MUplinkMsgHandler; @Autowired @Lazy private LwM2MOtaUpdateService otaUpdateService; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java index 735fa36e1e..ecc57a7458 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java @@ -85,6 +85,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.RejectedExecutionException; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -103,7 +104,7 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.ge import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.validateVersionedId; @Slf4j -@Service +@Service("lwM2mDownlinkMsgHandler") @TbLwM2mTransportComponent @RequiredArgsConstructor public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService implements LwM2mDownlinkMsgHandler { @@ -124,6 +125,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im @PreDestroy public void destroy() { + log.trace("Destroying {}", getClass().getSimpleName()); super.destroy(); } @@ -521,16 +523,22 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im private , T extends LwM2mResponse> void handleDownlinkError(LwM2mClient client, R request, DownlinkRequestCallback callback, Exception e) { log.trace("[{}] Received downlink error: {}.", client.getEndpoint(), e); - client.updateLastUplinkTime(); - executor.submit(() -> { - if (e instanceof TimeoutException || e instanceof ClientSleepingException) { - log.trace("[{}] Received {}, client is probably sleeping", client.getEndpoint(), e.getClass().getSimpleName()); - clientContext.asleep(client); - } else { - log.trace("[{}] Received {}", client.getEndpoint(), e.getClass().getSimpleName()); - } - callback.onError(toString(request), e); - }); + try { + client.updateLastUplinkTime(); + executor.submit(() -> { + if (e instanceof TimeoutException || e instanceof ClientSleepingException) { + log.trace("[{}] Received {}, client is probably sleeping", client.getEndpoint(), e.getClass().getSimpleName()); + clientContext.asleep(client); + } else { + log.trace("[{}] Received {}", client.getEndpoint(), e.getClass().getSimpleName()); + } + callback.onError(toString(request), e); + }); + } catch (RejectedExecutionException ree) { + log.warn("[{}] Can not handle downlink error. Executor already down", client.getEndpoint(), ree); + } catch (Exception exception) { + log.warn("[{}] Can not handle downlink error", client.getEndpoint(), exception); + } } private WriteRequest getWriteRequestSingleResource(ResourceModel.Type type, ContentFormat contentFormat, int objectId, int instanceId, int resourceId, Object value) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index 153e3894fb..f59643cbeb 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -146,6 +146,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl @PreDestroy public void destroy() { + log.trace("Destroying {}", getClass().getSimpleName()); super.destroy(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index c790d2495c..25e3a6b591 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -20,6 +20,8 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.model.ObjectModel; @@ -32,6 +34,7 @@ import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mResourceInstance; import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.request.*; import org.eclipse.leshan.core.request.WriteRequest.Mode; @@ -122,69 +125,41 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fr @Slf4j -@Service +@Service("lwM2mUplinkMsgHandler") @TbLwM2mTransportComponent +@RequiredArgsConstructor public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService implements LwM2mUplinkMsgHandler { - public LwM2mValueConverterImpl converter; + @Getter + private final LwM2mValueConverter converter = LwM2mValueConverterImpl.getInstance();; private final TransportService transportService; private final LwM2mTransportContext context; + @Lazy private final LwM2MAttributesService attributesService; private final LwM2MSessionManager sessionManager; + @Lazy private final LwM2MOtaUpdateService otaService; private final LwM2MTransportServerConfig config; private final LwM2MTelemetryLogService logService; private final LwM2mTransportServerHelper helper; private final TbLwM2MDtlsSessionStore sessionStore; private final LwM2mClientContext clientContext; - private final LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler; + private final LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler; //Do not use Lazy because we need live executor to handle msgs private final LwM2mVersionedModelProvider modelProvider; private final RegistrationStore registrationStore; private final TbLwM2mSecurityStore securityStore; private final LwM2MModelConfigService modelConfigService; - public DefaultLwM2mUplinkMsgHandler(TransportService transportService, - LwM2MTransportServerConfig config, - LwM2mTransportServerHelper helper, - LwM2mClientContext clientContext, - LwM2MTelemetryLogService logService, - LwM2MSessionManager sessionManager, - @Lazy LwM2MOtaUpdateService otaService, - @Lazy LwM2MAttributesService attributesService, - @Lazy LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler, - LwM2mTransportContext context, - TbLwM2MDtlsSessionStore sessionStore, - LwM2mVersionedModelProvider modelProvider, - RegistrationStore registrationStore, - TbLwM2mSecurityStore securityStore, - LwM2MModelConfigService modelConfigService) { - this.transportService = transportService; - this.sessionManager = sessionManager; - this.attributesService = attributesService; - this.otaService = otaService; - this.config = config; - this.helper = helper; - this.clientContext = clientContext; - this.logService = logService; - this.defaultLwM2MDownlinkMsgHandler = defaultLwM2MDownlinkMsgHandler; - this.context = context; - this.sessionStore = sessionStore; - this.modelProvider = modelProvider; - this.registrationStore = registrationStore; - this.securityStore = securityStore; - this.modelConfigService = modelConfigService; - } - @PostConstruct public void init() { super.init(); this.context.getScheduler().scheduleAtFixedRate(this::reportActivity, new Random().nextInt((int) config.getSessionReportTimeout()), config.getSessionReportTimeout(), TimeUnit.MILLISECONDS); - this.converter = LwM2mValueConverterImpl.getInstance(); } @PreDestroy public void destroy() { + log.trace("Destroying {}", getClass().getSimpleName()); super.destroy(); } @@ -955,6 +930,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * * @param lwM2MClient - LwM2M Client */ + @Override public void initAttributes(LwM2mClient lwM2MClient, boolean logFailedUpdateOfNonChangedValue) { Map keyNamesMap = this.getNamesFromProfileForSharedAttributes(lwM2MClient); if (!keyNamesMap.isEmpty()) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java index 0b2d819fa8..9beebe5279 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.lwm2m.server.uplink; +import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.request.CreateRequest; import org.eclipse.leshan.core.request.SendRequest; @@ -69,5 +70,10 @@ public interface LwM2mUplinkMsgHandler { void onToTransportUpdateCredentials(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ToTransportUpdateCredentialsProto updateCredentials); + void initAttributes(LwM2mClient lwM2MClient, boolean logFailedUpdateOfNonChangedValue); + LwM2MTransportServerConfig getConfig(); + + LwM2mValueConverter getConverter(); + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java index f207e7d05a..861261d951 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java @@ -52,7 +52,7 @@ import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdate import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdateState; import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult; import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateState; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import java.util.ArrayList; import java.util.Arrays; @@ -289,12 +289,12 @@ public class LwM2MTransportUtil { * Attribute pmax = new Attribute(MAXIMUM_PERIOD, "60"); * Attribute [] attrs = {gt, st}; */ - public static SimpleDownlinkRequest createWriteAttributeRequest(String target, Object params, DefaultLwM2mUplinkMsgHandler serviceImpl) { + public static SimpleDownlinkRequest createWriteAttributeRequest(String target, Object params, LwM2mUplinkMsgHandler serviceImpl) { AttributeSet attrSet = new AttributeSet(createWriteAttributes(params, serviceImpl, target)); return attrSet.getAttributes().size() > 0 ? new WriteAttributesRequest(target, attrSet) : null; } - private static Attribute[] createWriteAttributes(Object params, DefaultLwM2mUplinkMsgHandler serviceImpl, String target) { + private static Attribute[] createWriteAttributes(Object params, LwM2mUplinkMsgHandler serviceImpl, String target) { List attributeLists = new ArrayList<>(); Map map = JacksonUtil.convertValue(params, new TypeReference<>() { }); @@ -366,19 +366,19 @@ public class LwM2MTransportUtil { return newValues; } - public static Object convertWriteAttributes(String type, Object value, DefaultLwM2mUplinkMsgHandler serviceImpl, String target) { + public static Object convertWriteAttributes(String type, Object value, LwM2mUplinkMsgHandler serviceImpl, String target) { switch (type) { /** Integer [0:255]; */ case DIMENSION: - Long dim = (Long) serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); + Long dim = (Long) serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); return dim >= 0 && dim <= 255 ? dim : null; /**String;*/ case OBJECT_VERSION: - return serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), STRING, new LwM2mPath(target)); + return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), STRING, new LwM2mPath(target)); /**INTEGER */ case MINIMUM_PERIOD: case MAXIMUM_PERIOD: - return serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); + return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); /**Float; */ case GREATER_THAN: case LESSER_THAN: @@ -386,7 +386,7 @@ public class LwM2MTransportUtil { if (value.getClass().getSimpleName().equals("String")) { value = Double.valueOf((String) value); } - return serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), FLOAT, new LwM2mPath(target)); + return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), FLOAT, new LwM2mPath(target)); default: return null; } diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 9604110e13..ae0b35e44d 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -97,6 +97,19 @@ awaitility test + + com.google.protobuf + protobuf-java + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + 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 c64fe57b94..0d43fc6c93 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 @@ -17,11 +17,11 @@ package org.thingsboard.server.transport.mqtt; import com.fasterxml.jackson.databind.JsonNode; import com.google.gson.JsonParseException; +import com.google.protobuf.InvalidProtocolBufferException; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.mqtt.MqttConnAckMessage; -import io.netty.handler.codec.mqtt.MqttConnAckVariableHeader; import io.netty.handler.codec.mqtt.MqttConnectMessage; import io.netty.handler.codec.mqtt.MqttConnectReturnCode; import io.netty.handler.codec.mqtt.MqttFixedHeader; @@ -43,6 +43,8 @@ import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.ResponseCode; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -50,6 +52,8 @@ import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.ota.OtaPackageType; @@ -69,13 +73,20 @@ import org.thingsboard.server.common.transport.util.SslUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; +import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler; import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher; +import org.thingsboard.server.transport.mqtt.session.SparkplugNodeSessionHandler; import org.thingsboard.server.transport.mqtt.util.ReturnCode; import org.thingsboard.server.transport.mqtt.util.ReturnCodeResolver; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugRpcRequestHeader; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugRpcResponseBody; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic; import javax.net.ssl.SSLPeerUnverifiedException; import java.io.IOException; @@ -84,6 +95,7 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -95,15 +107,19 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.amazonaws.util.StringUtils.UTF8; -import static io.netty.handler.codec.mqtt.MqttMessageType.CONNACK; import static io.netty.handler.codec.mqtt.MqttMessageType.CONNECT; import static io.netty.handler.codec.mqtt.MqttMessageType.PINGRESP; import static io.netty.handler.codec.mqtt.MqttMessageType.SUBACK; -import static io.netty.handler.codec.mqtt.MqttMessageType.UNSUBACK; import static io.netty.handler.codec.mqtt.MqttQoS.AT_LEAST_ONCE; import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE; import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_RPC_ASYNC_MSG; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NDEATH; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.OFFLINE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.getTsKvProto; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.parseTopicPublish; /** * @author Andrew Shvayka @@ -120,7 +136,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private static final MqttQoS MAX_SUPPORTED_QOS_LVL = AT_LEAST_ONCE; private final UUID sessionId; - private final MqttTransportContext context; + protected final MqttTransportContext context; private final TransportService transportService; private final SchedulerComponent scheduler; private final SslHandler sslHandler; @@ -129,6 +145,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement final DeviceSessionCtx deviceSessionCtx; volatile InetSocketAddress address; volatile GatewaySessionHandler gatewaySessionHandler; + volatile SparkplugNodeSessionHandler sparkplugSessionHandler; private final ConcurrentHashMap otaPackSessions; private final ConcurrentHashMap chunkSizes; @@ -320,12 +337,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement String topicName = mqttMsg.variableHeader().topicName(); int msgId = mqttMsg.variableHeader().packetId(); log.trace("[{}][{}] Processing publish msg [{}][{}]!", sessionId, deviceSessionCtx.getDeviceId(), topicName, msgId); - if (topicName.startsWith(MqttTopics.BASE_GATEWAY_API_TOPIC)) { if (gatewaySessionHandler != null) { handleGatewayPublishMsg(ctx, topicName, msgId, mqttMsg); transportService.reportActivity(deviceSessionCtx.getSessionInfo()); + } else { + log.error("[gatewaySessionHandler] is null, [{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId); } + } else if (sparkplugSessionHandler != null) { + handleSparkplugPublishMsg(ctx, topicName, mqttMsg); } else { processDevicePublish(ctx, mqttMsg, topicName, msgId); } @@ -368,6 +388,46 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + private void handleSparkplugPublishMsg(ChannelHandlerContext ctx, String topicName, MqttPublishMessage mqttMsg) { + int msgId = mqttMsg.variableHeader().packetId(); + try { + SparkplugTopic sparkplugTopic = parseTopicPublish(topicName); + if (sparkplugTopic.isNode()) { + // A node topic + SparkplugBProto.Payload sparkplugBProtoNode = SparkplugBProto.Payload.parseFrom(ProtoMqttAdaptor.toBytes(mqttMsg.payload())); + switch (sparkplugTopic.getType()) { + case NBIRTH: + case NCMD: + case NDATA: + sparkplugSessionHandler.onAttributesTelemetryProto(msgId, sparkplugBProtoNode, deviceSessionCtx.getDeviceInfo().getDeviceName(), sparkplugTopic); + break; + default: + } + } else { + // A device topic + SparkplugBProto.Payload sparkplugBProtoDevice = SparkplugBProto.Payload.parseFrom(ProtoMqttAdaptor.toBytes(mqttMsg.payload())); + switch (sparkplugTopic.getType()) { + case DBIRTH: + case DCMD: + case DDATA: + sparkplugSessionHandler.onAttributesTelemetryProto(msgId, sparkplugBProtoDevice, sparkplugTopic.getDeviceId(), sparkplugTopic); + break; + case DDEATH: + sparkplugSessionHandler.onDeviceDisconnect(mqttMsg, sparkplugTopic.getDeviceId()); + break; + default: + } + } + } catch (RuntimeException e) { + log.error("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); + ack(ctx, msgId, ReturnCode.IMPLEMENTATION_SPECIFIC); + ctx.close(); + } catch (AdaptorException | ThingsboardException | InvalidProtocolBufferException e) { + log.error("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); + sendAckOrCloseSession(ctx, topicName, msgId); + } + } + private void processDevicePublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg, String topicName, int msgId) { try { Matcher fwMatcher; @@ -628,69 +688,74 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement String topic = subscription.topicName(); MqttQoS reqQoS = subscription.qualityOfService(); try { - switch (topic) { - case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: { - processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); - activityReported = true; - break; - } - case MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC: { - processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2); - activityReported = true; - break; - } - case MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC: { - processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_JSON); - activityReported = true; - break; - } - case MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC: { - processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_PROTO); - activityReported = true; - break; - } - case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: { - processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); - activityReported = true; - break; - } - case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC: { - processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2); - activityReported = true; - break; - } - case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC: { - processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_JSON); - activityReported = true; - break; - } - case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC: { - processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_PROTO); - activityReported = true; - break; + if (sparkplugSessionHandler != null) { + sparkplugSessionHandler.handleSparkplugSubscribeMsg(grantedQoSList, subscription, reqQoS); + activityReported = true; + } else { + switch (topic) { + case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: { + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); + activityReported = true; + break; + } + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC: { + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2); + activityReported = true; + break; + } + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC: { + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_JSON); + activityReported = true; + break; + } + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC: { + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_PROTO); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: { + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC: { + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC: { + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_JSON); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC: { + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_PROTO); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC: + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_TOPIC: + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_JSON_TOPIC: + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_PROTO_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_PROTO_TOPIC: + case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: + case MqttTopics.GATEWAY_RPC_TOPIC: + case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC: + case MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC: + case MqttTopics.DEVICE_FIRMWARE_RESPONSES_TOPIC: + case MqttTopics.DEVICE_FIRMWARE_ERROR_TOPIC: + case MqttTopics.DEVICE_SOFTWARE_RESPONSES_TOPIC: + case MqttTopics.DEVICE_SOFTWARE_ERROR_TOPIC: + registerSubQoS(topic, grantedQoSList, reqQoS); + break; + default: + log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS); + grantedQoSList.add(ReturnCodeResolver.getSubscriptionReturnCode(deviceSessionCtx.getMqttVersion(), ReturnCode.TOPIC_FILTER_INVALID)); + break; } - case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC: - case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_TOPIC: - case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_JSON_TOPIC: - case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_PROTO_TOPIC: - case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC: - case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC: - case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC: - case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_PROTO_TOPIC: - case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: - case MqttTopics.GATEWAY_RPC_TOPIC: - case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC: - case MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC: - case MqttTopics.DEVICE_FIRMWARE_RESPONSES_TOPIC: - case MqttTopics.DEVICE_FIRMWARE_ERROR_TOPIC: - case MqttTopics.DEVICE_SOFTWARE_RESPONSES_TOPIC: - case MqttTopics.DEVICE_SOFTWARE_ERROR_TOPIC: - registerSubQoS(topic, grantedQoSList, reqQoS); - break; - default: - log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS); - grantedQoSList.add(ReturnCodeResolver.getSubscriptionReturnCode(deviceSessionCtx.getMqttVersion(), ReturnCode.TOPIC_FILTER_INVALID)); - break; } } catch (Exception e) { log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS, e); @@ -715,7 +780,16 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement registerSubQoS(topic, grantedQoSList, reqQoS); } - private void registerSubQoS(String topic, List grantedQoSList, MqttQoS reqQoS) { + public void processAttributesRpcSubscribeSparkplugNode(List grantedQoSList, MqttQoS reqQoS) { + transportService.process(TransportProtos.TransportToDeviceActorMsg.newBuilder() + .setSessionInfo(deviceSessionCtx.getSessionInfo()) + .setSubscribeToAttributes(SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG) + .setSubscribeToRPC(SUBSCRIBE_TO_RPC_ASYNC_MSG) + .build(), null); + registerSubQoS(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, grantedQoSList, reqQoS); + } + + public void registerSubQoS(String topic, List grantedQoSList, MqttQoS reqQoS) { grantedQoSList.add(getMinSupportedQos(reqQoS)); mqttQoSMap.put(new MqttTopicMatcher(topic), getMinSupportedQos(reqQoS)); } @@ -986,6 +1060,39 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + private void checkSparkplugNodeSession(MqttConnectMessage connectMessage, ChannelHandlerContext ctx) { + try { + if (sparkplugSessionHandler == null) { + SparkplugTopic sparkplugTopicNode = validatedSparkplugTopicConnectedNode(connectMessage); + if (sparkplugTopicNode != null) { + SparkplugBProto.Payload sparkplugBProtoNode = SparkplugBProto.Payload.parseFrom(connectMessage.payload().willMessageInBytes()); + sparkplugSessionHandler = new SparkplugNodeSessionHandler(this, deviceSessionCtx, sessionId, sparkplugTopicNode); + sparkplugSessionHandler.onAttributesTelemetryProto(0, sparkplugBProtoNode, + deviceSessionCtx.getDeviceInfo().getDeviceName(), sparkplugTopicNode); + } else { + log.trace("[{}][{}] Failed to fetch sparkplugDevice connect: sparkplugTopicName without SparkplugMessageType.NDEATH.", sessionId, deviceSessionCtx.getDeviceInfo().getDeviceName()); + throw new ThingsboardException("Invalid request body", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } + } catch (Exception e) { + log.trace("[{}][{}] Failed to fetch sparkplugDevice connect, sparkplugTopicName", sessionId, deviceSessionCtx.getDeviceInfo().getDeviceName(), e); + ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SERVER_UNAVAILABLE_5, connectMessage)); + ctx.close(); + } + } + + private SparkplugTopic validatedSparkplugTopicConnectedNode(MqttConnectMessage connectMessage) throws ThingsboardException { + if (StringUtils.isNotBlank(connectMessage.payload().willTopic()) + && connectMessage.payload().willMessageInBytes() != null + && connectMessage.payload().willMessageInBytes().length > 0) { + SparkplugTopic sparkplugTopicNode = parseTopicPublish(connectMessage.payload().willTopic()); + if (NDEATH.equals(sparkplugTopicNode.getType())) { + return sparkplugTopicNode; + } + } + return null; + } + @Override public void operationComplete(Future future) throws Exception { log.trace("[{}] Channel closed!", sessionId); @@ -998,14 +1105,19 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_CLOSED, null); transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); if (gatewaySessionHandler != null) { - gatewaySessionHandler.onGatewayDisconnect(); + gatewaySessionHandler.onDevicesDisconnect(); + } + if (sparkplugSessionHandler != null) { + // add Msg Telemetry node: key STATE type: String value: OFFLINE ts: sparkplugBProto.getTimestamp() + sparkplugSessionHandler.sendSparkplugStateOnTelemetry(deviceSessionCtx.getSessionInfo(), + deviceSessionCtx.getDeviceInfo().getDeviceName(), OFFLINE, new Date().getTime()); + sparkplugSessionHandler.onDevicesDisconnect(); } deviceSessionCtx.setDisconnected(); } deviceSessionCtx.release(); } - private void onValidateDeviceResponse(ValidateDeviceCredentialsResponse msg, ChannelHandlerContext ctx, MqttConnectMessage connectMessage) { if (!msg.hasDeviceInfo()) { context.onAuthFailure(address); @@ -1032,7 +1144,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onSuccess(Void msg) { SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); - checkGatewaySession(sessionMetaData); + if (deviceSessionCtx.isSparkplug()) { + checkSparkplugNodeSession(connectMessage, ctx); + } else { + checkGatewaySession(sessionMetaData); + } ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SUCCESS, connectMessage)); deviceSessionCtx.setConnected(true); log.debug("[{}] Client connected!", sessionId); @@ -1068,10 +1184,24 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg notification) { log.trace("[{}] Received attributes update notification to device", sessionId); - String topic = attrSubTopicType.getAttributesSubTopic(); - MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(attrSubTopicType); try { - adaptor.convertToPublish(deviceSessionCtx, notification, topic).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + if (sparkplugSessionHandler != null) { + log.trace("[{}] Received attributes update notification to sparkplug device", sessionId); + notification.getSharedUpdatedList().forEach(tsKvProto -> { + if (sparkplugSessionHandler.getNodeBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { + SparkplugTopic sparkplugTopic = new SparkplugTopic(sparkplugSessionHandler.getSparkplugTopicNode(), + SparkplugMessageType.NCMD); + sparkplugSessionHandler.createSparkplugMqttPublishMsg(tsKvProto, + sparkplugTopic.toString(), + sparkplugSessionHandler.getNodeBirthMetrics().get(tsKvProto.getKv().getKey())) + .ifPresent(sparkplugSessionHandler::writeAndFlush); + } + }); + } else { + String topic = attrSubTopicType.getAttributesSubTopic(); + MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(attrSubTopicType); + adaptor.convertToPublish(deviceSessionCtx, notification, topic).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + } } catch (Exception e) { log.trace("[{}] Failed to convert device attributes update to MQTT msg", sessionId, e); } @@ -1080,47 +1210,84 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onRemoteSessionCloseCommand(UUID sessionId, TransportProtos.SessionCloseNotificationProto sessionCloseNotification) { log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage()); + transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); deviceSessionCtx.getChannel().close(); } @Override public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { log.trace("[{}] Received RPC command to device", sessionId); - String baseTopic = rpcSubTopicType.getRpcRequestTopicBase(); - MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(rpcSubTopicType); try { - adaptor.convertToPublish(deviceSessionCtx, rpcRequest, baseTopic).ifPresent(payload -> { - int msgId = ((MqttPublishMessage) payload).variableHeader().packetId(); - if (isAckExpected(payload)) { - rpcAwaitingAck.put(msgId, rpcRequest); - context.getScheduler().schedule(() -> { - TransportProtos.ToDeviceRpcRequestMsg msg = rpcAwaitingAck.remove(msgId); - if (msg != null) { - transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, RpcStatus.TIMEOUT, TransportServiceCallback.EMPTY); - } - }, Math.max(0, Math.min(deviceSessionCtx.getContext().getTimeout(), rpcRequest.getExpirationTime() - System.currentTimeMillis())), TimeUnit.MILLISECONDS); - } - var cf = publish(payload, deviceSessionCtx); - cf.addListener(result -> { - if (result.cause() == null) { - if (!isAckExpected(payload)) { - transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); - } else if (rpcRequest.getPersisted()) { - transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, RpcStatus.SENT, TransportServiceCallback.EMPTY); - } - } else { - // TODO: send error - } - }); - }); + if (sparkplugSessionHandler != null) { + handleToSparkplugDeviceRpcRequest(rpcRequest); + } else { + String baseTopic = rpcSubTopicType.getRpcRequestTopicBase(); + MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(rpcSubTopicType); + adaptor.convertToPublish(deviceSessionCtx, rpcRequest, baseTopic) + .ifPresent(payload -> sendToDeviceRpcRequest(payload, rpcRequest, deviceSessionCtx.getSessionInfo())); + } } catch (Exception e) { - transportService.process(deviceSessionCtx.getSessionInfo(), - TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setRequestId(rpcRequest.getRequestId()).setError("Failed to convert device RPC command to MQTT msg").build(), TransportServiceCallback.EMPTY); log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e); + this.sendErrorRpcResponse(deviceSessionCtx.getSessionInfo(), rpcRequest.getRequestId(), + ThingsboardErrorCode.INVALID_ARGUMENTS, + "Failed to convert device RPC command to MQTT msg: " + rpcRequest.getMethodName() + rpcRequest.getParams()); } } + private void handleToSparkplugDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws ThingsboardException { + SparkplugMessageType messageType = SparkplugMessageType.parseMessageType(rpcRequest.getMethodName()); + SparkplugRpcRequestHeader header; + if (StringUtils.isNotEmpty(rpcRequest.getParams())) { + header = JacksonUtil.fromString(rpcRequest.getParams(), SparkplugRpcRequestHeader.class); + } else { + header = new SparkplugRpcRequestHeader(); + } + header.setMessageType(messageType.name()); + TransportProtos.TsKvProto tsKvProto = getTsKvProto(header.getMetricName(), header.getValue(), new Date().getTime()); + if (sparkplugSessionHandler.getNodeBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { + SparkplugTopic sparkplugTopic = new SparkplugTopic(sparkplugSessionHandler.getSparkplugTopicNode(), + messageType); + sparkplugSessionHandler.createSparkplugMqttPublishMsg(tsKvProto, + sparkplugTopic.toString(), + sparkplugSessionHandler.getNodeBirthMetrics().get(tsKvProto.getKv().getKey())) + .ifPresent(payload -> sendToDeviceRpcRequest(payload, rpcRequest, deviceSessionCtx.getSessionInfo())); + } else { + sendErrorRpcResponse(deviceSessionCtx.getSessionInfo(), rpcRequest.getRequestId(), + ThingsboardErrorCode.BAD_REQUEST_PARAMS, "Failed send To Node Rpc Request: " + + rpcRequest.getMethodName() + ". This node does not have a metricName: [" + tsKvProto.getKv().getKey() + "]"); + } + } + + public void sendToDeviceRpcRequest(MqttMessage payload, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, TransportProtos.SessionInfoProto sessionInfo) { + int msgId = ((MqttPublishMessage) payload).variableHeader().packetId(); + if (isAckExpected(payload)) { + rpcAwaitingAck.put(msgId, rpcRequest); + context.getScheduler().schedule(() -> { + TransportProtos.ToDeviceRpcRequestMsg msg = rpcAwaitingAck.remove(msgId); + if (msg != null) { + transportService.process(sessionInfo, rpcRequest, RpcStatus.TIMEOUT, TransportServiceCallback.EMPTY); + } + }, Math.max(0, Math.min(deviceSessionCtx.getContext().getTimeout(), rpcRequest.getExpirationTime() - System.currentTimeMillis())), TimeUnit.MILLISECONDS); + } + var cf = publish(payload, deviceSessionCtx); + cf.addListener(result -> { + if (result.cause() == null) { + if (!isAckExpected(payload)) { + transportService.process(sessionInfo, rpcRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); + } else if (rpcRequest.getPersisted()) { + transportService.process(sessionInfo, rpcRequest, RpcStatus.SENT, TransportServiceCallback.EMPTY); + } + if (sparkplugSessionHandler != null) { + this.sendSuccessRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.CONTENT, "Success: " + rpcRequest.getMethodName()); + } + } else { + log.trace("[{}] Failed send To Device Rpc Request [{}]", sessionId, rpcRequest.getMethodName()); + this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), + ThingsboardErrorCode.INVALID_ARGUMENTS, " Failed send To Device Rpc Request: " + rpcRequest.getMethodName()); + } + }); + } + @Override public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg rpcResponse) { log.trace("[{}] Received RPC response from server", sessionId); @@ -1158,4 +1325,16 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement ctx.close(); } + public void sendErrorRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ThingsboardErrorCode result, String errorMsg) { + String payload = JacksonUtil.toString(SparkplugRpcResponseBody.builder().result(result.name()).error(errorMsg).build()); + TransportProtos.ToDeviceRpcResponseMsg msg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setError(payload).build(); + transportService.process(sessionInfo, msg, null); + } + + public void sendSuccessRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ResponseCode result, String successMsg) { + String payload = JacksonUtil.toString(SparkplugRpcResponseBody.builder().result(result.getName()).result(successMsg).build()); + TransportProtos.ToDeviceRpcResponseMsg msg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setError(payload).build(); + transportService.process(sessionInfo, msg, null); + } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java similarity index 93% rename from common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java rename to common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java index a12a590610..e265a4c2f1 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java @@ -35,14 +35,14 @@ import java.util.concurrent.ConcurrentMap; * Created by ashvayka on 19.01.17. */ @Slf4j -public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext implements SessionMsgListener { +public abstract class AbstractGatewayDeviceSessionContext extends MqttDeviceAwareSessionContext implements SessionMsgListener { - private final GatewaySessionHandler parent; + protected final T parent; private final TransportService transportService; - public GatewayDeviceSessionCtx(GatewaySessionHandler parent, TransportDeviceInfo deviceInfo, - DeviceProfile deviceProfile, ConcurrentMap mqttQoSMap, - TransportService transportService) { + public AbstractGatewayDeviceSessionContext(T parent, TransportDeviceInfo deviceInfo, + DeviceProfile deviceProfile, ConcurrentMap mqttQoSMap, + TransportService transportService) { super(UUID.randomUUID(), mqttQoSMap); this.parent = parent; setSessionInfo(SessionInfoProto.newBuilder() diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java new file mode 100644 index 0000000000..2bb59dc9e8 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -0,0 +1,767 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.session; + + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.ProtocolStringList; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttPublishMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.adaptor.ProtoConverter; +import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; +import org.thingsboard.server.transport.mqtt.MqttTransportContext; +import org.thingsboard.server.transport.mqtt.MqttTransportHandler; +import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; +import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; +import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; +import org.thingsboard.server.transport.mqtt.util.ReturnCode; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_RPC_ASYNC_MSG; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.OFFLINE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; + +/** + * Created by ashvayka on 19.01.17. + */ +@Slf4j +public abstract class AbstractGatewaySessionHandler { + + protected static final String DEFAULT_DEVICE_TYPE = "default"; + private static final String CAN_T_PARSE_VALUE = "Can't parse value: "; + private static final String DEVICE_PROPERTY = "device"; + + protected final MqttTransportContext context; + protected final TransportService transportService; + protected final TransportDeviceInfo gateway; + protected final UUID sessionId; + private final ConcurrentMap deviceCreationLockMap; + private final ConcurrentMap devices; + private final ConcurrentMap> deviceFutures; + protected final ConcurrentMap mqttQoSMap; + protected final ChannelHandlerContext channel; + protected final DeviceSessionCtx deviceSessionCtx; + + public AbstractGatewaySessionHandler(DeviceSessionCtx deviceSessionCtx, UUID sessionId) { + this.context = deviceSessionCtx.getContext(); + this.transportService = context.getTransportService(); + this.deviceSessionCtx = deviceSessionCtx; + this.gateway = deviceSessionCtx.getDeviceInfo(); + this.sessionId = sessionId; + this.devices = new ConcurrentHashMap<>(); + this.deviceFutures = new ConcurrentHashMap<>(); + this.deviceCreationLockMap = createWeakMap(); + this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap(); + this.channel = deviceSessionCtx.getChannel(); + } + + ConcurrentReferenceHashMap createWeakMap() { + return new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK); + } + + public void onDeviceDisconnect(MqttPublishMessage mqttMsg) throws AdaptorException { + if (isJsonPayloadType()) { + onDeviceDisconnectJson(mqttMsg); + } else { + onGatewayDeviceDisconnectProto(mqttMsg); + } + } + + public void onDeviceClaim(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceClaimJson(msgId, payload); + } else { + onDeviceClaimProto(msgId, payload); + } + } + + public void onDeviceAttributes(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceAttributesJson(msgId, payload); + } else { + onDeviceAttributesProto(msgId, payload); + } + } + + public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException { + if (isJsonPayloadType()) { + onDeviceAttributesRequestJson(mqttMsg); + } else { + onDeviceAttributesRequestProto(mqttMsg); + } + } + + public void onDeviceRpcResponse(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceRpcResponseJson(msgId, payload); + } else { + onDeviceRpcResponseProto(msgId, payload); + } + } + + public void onDevicesDisconnect() { + devices.forEach(this::deregisterSession); + } + + public void onDeviceDeleted(String deviceName) { + deregisterSession(deviceName); + } + + public String getNodeId() { + return context.getNodeId(); + } + + public UUID getSessionId() { + return sessionId; + } + + public MqttTransportAdaptor getPayloadAdaptor() { + return deviceSessionCtx.getPayloadAdaptor(); + } + + void deregisterSession(String deviceName) { + MqttDeviceAwareSessionContext deviceSessionCtx = devices.remove(deviceName); + if (deviceSessionCtx != null) { + deregisterSession(deviceName, deviceSessionCtx); + } else { + log.debug("[{}] Device [{}] was already removed from the gateway session", sessionId, deviceName); + } + } + + public ChannelFuture writeAndFlush(MqttMessage mqttMessage) { + return channel.writeAndFlush(mqttMessage); + } + + int nextMsgId() { + return deviceSessionCtx.nextMsgId(); + } + + protected boolean isJsonPayloadType() { + return deviceSessionCtx.isJsonPayloadType(); + } + + protected void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) { + log.trace("[{}] onDeviceConnect: {}", sessionId, deviceName); + Futures.addCallback(onDeviceConnect(deviceName, deviceType), new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T result) { + ack(msg, ReturnCode.SUCCESS); + log.trace("[{}] onDeviceConnectOk: {}", sessionId, deviceName); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Failed to process device connect command: {}", sessionId, deviceName, t); + + } + }, context.getExecutor()); + } + + ListenableFuture onDeviceConnect(String deviceName, String deviceType) { + T result = devices.get(deviceName); + if (result == null) { + Lock deviceCreationLock = deviceCreationLockMap.computeIfAbsent(deviceName, s -> new ReentrantLock()); + deviceCreationLock.lock(); + try { + result = devices.get(deviceName); + if (result == null) { + return getDeviceCreationFuture(deviceName, deviceType); + } else { + return Futures.immediateFuture(result); + } + } finally { + deviceCreationLock.unlock(); + } + } else { + return Futures.immediateFuture(result); + } + } + + private ListenableFuture getDeviceCreationFuture(String deviceName, String deviceType) { + final SettableFuture futureToSet = SettableFuture.create(); + ListenableFuture future = deviceFutures.putIfAbsent(deviceName, futureToSet); + if (future != null) { + return future; + } + try { + transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() + .setDeviceName(deviceName) + .setDeviceType(deviceType) + .setGatewayIdMSB(gateway.getDeviceId().getId().getMostSignificantBits()) + .setGatewayIdLSB(gateway.getDeviceId().getId().getLeastSignificantBits()) + .setSparkplug(this.deviceSessionCtx.isSparkplug()) + .build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(GetOrCreateDeviceFromGatewayResponse msg) { + T deviceSessionCtx = newDeviceSessionCtx(msg); + if (devices.putIfAbsent(deviceName, deviceSessionCtx) == null) { + log.trace("[{}] First got or created device [{}], type [{}] for the gateway session", sessionId, deviceName, deviceType); + SessionInfoProto deviceSessionInfo = deviceSessionCtx.getSessionInfo(); + transportService.registerAsyncSession(deviceSessionInfo, deviceSessionCtx); + transportService.process(TransportProtos.TransportToDeviceActorMsg.newBuilder() + .setSessionInfo(deviceSessionInfo) + .setSessionEvent(SESSION_EVENT_MSG_OPEN) + .setSubscribeToAttributes(SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG) + .setSubscribeToRPC(SUBSCRIBE_TO_RPC_ASYNC_MSG) + .build(), null); + } + futureToSet.set(devices.get(deviceName)); + deviceFutures.remove(deviceName); + } + + @Override + public void onError(Throwable e) { + log.warn("[{}] Failed to process device connect command: {}", sessionId, deviceName, e); + futureToSet.setException(e); + deviceFutures.remove(deviceName); + } + }); + return futureToSet; + } catch (Throwable e) { + deviceFutures.remove(deviceName); + throw e; + } + } + + protected abstract T newDeviceSessionCtx(GetOrCreateDeviceFromGatewayResponse msg); + + protected int getMsgId(MqttPublishMessage mqttMsg) { + return mqttMsg.variableHeader().packetId(); + } + + protected void onDeviceConnectJson(MqttPublishMessage mqttMsg) throws AdaptorException { + JsonElement json = getJson(mqttMsg); + String deviceName = checkDeviceName(getDeviceName(json)); + String deviceType = getDeviceType(json); + processOnConnect(mqttMsg, deviceName, deviceType); + } + + protected void onDeviceConnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { + try { + TransportApiProtos.ConnectMsg connectProto = TransportApiProtos.ConnectMsg.parseFrom(getBytes(mqttMsg.payload())); + String deviceName = checkDeviceName(connectProto.getDeviceName()); + String deviceType = StringUtils.isEmpty(connectProto.getDeviceType()) ? DEFAULT_DEVICE_TYPE : connectProto.getDeviceType(); + processOnConnect(mqttMsg, deviceName, deviceType); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void onDeviceDisconnectJson(MqttPublishMessage msg) throws AdaptorException { + String deviceName = checkDeviceName(getDeviceName(getJson(msg))); + processOnDisconnect(msg, deviceName); + } + + protected void onGatewayDeviceDisconnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { + try { + TransportApiProtos.DisconnectMsg connectProto = TransportApiProtos.DisconnectMsg.parseFrom(getBytes(mqttMsg.payload())); + String deviceName = checkDeviceName(connectProto.getDeviceName()); + processOnDisconnect(mqttMsg, deviceName); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + void processOnDisconnect(MqttPublishMessage msg, String deviceName) { + deregisterSession(deviceName); + ack(msg, ReturnCode.SUCCESS); + } + + protected void onDeviceTelemetryJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); + if (json.isJsonObject()) { + JsonObject jsonObj = json.getAsJsonObject(); + for (Map.Entry deviceEntry : jsonObj.entrySet()) { + String deviceName = deviceEntry.getKey(); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + if (!deviceEntry.getValue().isJsonArray()) { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + try { + TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray()); + processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); + channel.close(); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + } + + protected void onDeviceTelemetryProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayTelemetryMsg telemetryMsgProto = TransportApiProtos.GatewayTelemetryMsg.parseFrom(getBytes(payload)); + List deviceMsgList = telemetryMsgProto.getMsgList(); + if (!CollectionUtils.isEmpty(deviceMsgList)) { + deviceMsgList.forEach(telemetryMsg -> { + String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + TransportProtos.PostTelemetryMsg msg = telemetryMsg.getMsg(); + try { + TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray()); + processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); + channel.close(); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices telemetry messages is empty for: [{}]", sessionId, gateway.getDeviceId()); + throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry messages is empty for [" + gateway.getDeviceId() + "]"); + } + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + public void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg postTelemetryMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); + } + + public TransportProtos.PostTelemetryMsg postTelemetryMsgCreated(TransportProtos.KeyValueProto keyValueProto, long ts) { + List result = new ArrayList<>(); + result.add(keyValueProto); + TransportProtos.PostTelemetryMsg.Builder request = TransportProtos.PostTelemetryMsg.newBuilder(); + TransportProtos.TsKvListProto.Builder builder = TransportProtos.TsKvListProto.newBuilder(); + builder.setTs(ts); + builder.addAllKv(result); + request.addTsKvList(builder.build()); + return request.build(); + } + + private void onDeviceClaimJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); + if (json.isJsonObject()) { + JsonObject jsonObj = json.getAsJsonObject(); + for (Map.Entry deviceEntry : jsonObj.entrySet()) { + String deviceName = deviceEntry.getKey(); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + if (!deviceEntry.getValue().isJsonObject()) { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + try { + DeviceId deviceId = deviceCtx.getDeviceId(); + TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, deviceEntry.getValue()); + processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + } + + private void onDeviceClaimProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayClaimMsg claimMsgProto = TransportApiProtos.GatewayClaimMsg.parseFrom(getBytes(payload)); + List claimMsgList = claimMsgProto.getMsgList(); + if (!CollectionUtils.isEmpty(claimMsgList)) { + claimMsgList.forEach(claimDeviceMsg -> { + String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + TransportApiProtos.ClaimDevice claimRequest = claimDeviceMsg.getClaimRequest(); + if (claimRequest == null) { + throw new IllegalArgumentException("Claim request for device: " + deviceName + " is null!"); + } + try { + DeviceId deviceId = deviceCtx.getDeviceId(); + TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray()); + processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, claimRequest, e); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices claim messages is empty for: [{}]", sessionId, gateway.getDeviceId()); + throw new IllegalArgumentException("[" + sessionId + "] Devices claim messages is empty for [" + gateway.getDeviceId() + "]"); + } + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.ClaimDeviceMsg claimDeviceMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + } + + private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); + if (json.isJsonObject()) { + JsonObject jsonObj = json.getAsJsonObject(); + for (Map.Entry deviceEntry : jsonObj.entrySet()) { + String deviceName = deviceEntry.getKey(); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + if (!deviceEntry.getValue().isJsonObject()) { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject()); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + } + + private void onDeviceAttributesProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayAttributesMsg attributesMsgProto = TransportApiProtos.GatewayAttributesMsg.parseFrom(getBytes(payload)); + List attributesMsgList = attributesMsgProto.getMsgList(); + if (!CollectionUtils.isEmpty(attributesMsgList)) { + attributesMsgList.forEach(attributesMsg -> { + String deviceName = checkDeviceName(attributesMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); + if (kvListProto == null) { + throw new IllegalArgumentException("Attributes List for device: " + deviceName + " is empty!"); + } + try { + TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); + throw new IllegalArgumentException("[" + sessionId + "] Devices attributes keys list is empty for [" + gateway.getDeviceId() + "]"); + } + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg postAttributeMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + } + + private void onDeviceAttributesRequestJson(MqttPublishMessage msg) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, msg.payload()); + if (json.isJsonObject()) { + JsonObject jsonObj = json.getAsJsonObject(); + int requestId = jsonObj.get("id").getAsInt(); + String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); + boolean clientScope = jsonObj.get("client").getAsBoolean(); + Set keys; + if (jsonObj.has("key")) { + keys = Collections.singleton(jsonObj.get("key").getAsString()); + } else { + JsonArray keysArray = jsonObj.get("keys").getAsJsonArray(); + keys = new HashSet<>(); + for (JsonElement keyObj : keysArray) { + keys.add(keyObj.getAsString()); + } + } + TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); + processGetAttributeRequestMessage(msg, deviceName, requestMsg); + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + } + + private void onDeviceAttributesRequestProto(MqttPublishMessage mqttMsg) throws AdaptorException { + try { + TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = TransportApiProtos.GatewayAttributesRequestMsg.parseFrom(getBytes(mqttMsg.payload())); + String deviceName = checkDeviceName(gatewayAttributesRequestMsg.getDeviceName()); + int requestId = gatewayAttributesRequestMsg.getId(); + boolean clientScope = gatewayAttributesRequestMsg.getClient(); + ProtocolStringList keysList = gatewayAttributesRequestMsg.getKeysList(); + Set keys = new HashSet<>(keysList); + TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); + processGetAttributeRequestMessage(mqttMsg, deviceName, requestMsg); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void onDeviceRpcResponseJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); + if (json.isJsonObject()) { + JsonObject jsonObj = json.getAsJsonObject(); + String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + Integer requestId = jsonObj.get("id").getAsInt(); + String data = jsonObj.get("data").toString(); + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() + .setRequestId(requestId).setPayload(data).build(); + processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + } + + private void onDeviceRpcResponseProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.parseFrom(getBytes(payload)); + String deviceName = checkDeviceName(gatewayRpcResponseMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + Integer requestId = gatewayRpcResponseMsg.getId(); + String data = gatewayRpcResponseMsg.getData(); + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() + .setRequestId(requestId).setPayload(data).build(); + processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); + } + + private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) { + int msgId = getMsgId(mqttMsg); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); + } + + @Override + public void onFailure(Throwable t) { + ack(mqttMsg, ReturnCode.IMPLEMENTATION_SPECIFIC); + log.debug("[{}] Failed to process device attributes request command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } + + private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set keys) { + TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); + result.setRequestId(requestId); + + if (clientScope) { + result.addAllClientAttributeNames(keys); + } else { + result.addAllSharedAttributeNames(keys); + } + return result.build(); + } + + protected ListenableFuture checkDeviceConnected(String deviceName) { + T ctx = devices.get(deviceName); + if (ctx == null) { + log.debug("[{}] Missing device [{}] for the gateway session", sessionId, deviceName); + return onDeviceConnect(deviceName, DEFAULT_DEVICE_TYPE); + } else { + return Futures.immediateFuture(ctx); + } + } + + protected String checkDeviceName(String deviceName) { + if (StringUtils.isEmpty(deviceName)) { + throw new RuntimeException("Device name is empty!"); + } else { + return deviceName; + } + } + + private String getDeviceName(JsonElement json) { + return json.getAsJsonObject().get(DEVICE_PROPERTY).getAsString(); + } + + private String getDeviceType(JsonElement json) { + JsonElement type = json.getAsJsonObject().get("type"); + return type == null || type instanceof JsonNull ? DEFAULT_DEVICE_TYPE : type.getAsString(); + } + + private JsonElement getJson(MqttPublishMessage mqttMsg) throws AdaptorException { + return JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); + } + + protected byte[] getBytes(ByteBuf payload) { + return ProtoMqttAdaptor.toBytes(payload); + } + + protected void ack(MqttPublishMessage msg, ReturnCode returnCode) { + int msgId = getMsgId(msg); + if (msgId > 0) { + writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, returnCode)); + } + } + + private void deregisterSession(String deviceName, MqttDeviceAwareSessionContext deviceSessionCtx) { + if (this.deviceSessionCtx.isSparkplug()) { + sendSparkplugStateOnTelemetry(deviceSessionCtx.getSessionInfo(), + deviceSessionCtx.getDeviceInfo().getDeviceName(), OFFLINE, new Date().getTime()); + } + transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); + transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_CLOSED, null); + log.debug("[{}] Removed device [{}] from the gateway session", sessionId, deviceName); + } + + public void sendSparkplugStateOnTelemetry(TransportProtos.SessionInfoProto sessionInfo, String deviceName, SparkplugConnectionState connectionState, long ts) { + TransportProtos.KeyValueProto.Builder keyValueProtoBuilder = TransportProtos.KeyValueProto.newBuilder(); + keyValueProtoBuilder.setKey(messageName(STATE)); + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.STRING_V); + keyValueProtoBuilder.setStringV(connectionState.name()); + TransportProtos.PostTelemetryMsg postTelemetryMsg = postTelemetryMsgCreated(keyValueProtoBuilder.build(), ts); + transportService.process(sessionInfo, postTelemetryMsg, getPubAckCallback(channel, deviceName, -1, postTelemetryMsg)); + } + + private TransportServiceCallback getPubAckCallback(final ChannelHandlerContext ctx, final String deviceName, final int msgId, final T msg) { + return new TransportServiceCallback() { + @Override + public void onSuccess(Void dummy) { + log.trace("[{}][{}] Published msg: {}", sessionId, deviceName, msg); + if (msgId > 0) { + ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, ReturnCode.SUCCESS)); + } + } + + @Override + public void onError(Throwable e) { + log.trace("[{}] Failed to publish msg: {} for device: {}", sessionId, msg, deviceName, e); + ctx.close(); + } + }; + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java new file mode 100644 index 0000000000..e94a98d287 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.session; + +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; + +import java.util.concurrent.ConcurrentMap; + +/** + * Created by nickAS21 on 26.12.22 + */ +public class GatewayDeviceSessionContext extends AbstractGatewayDeviceSessionContext { + + public GatewayDeviceSessionContext(GatewaySessionHandler parent, + TransportDeviceInfo deviceInfo, + DeviceProfile deviceProfile, + ConcurrentMap mqttQoSMap, + TransportService transportService) { + super(parent, deviceInfo, deviceProfile, mqttQoSMap, transportService); + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java index bf40cb8439..de0f2a502b 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java @@ -15,100 +15,20 @@ */ package org.thingsboard.server.transport.mqtt.session; - -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonObject; -import com.google.gson.JsonSyntaxException; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.ProtocolStringList; import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; -import lombok.extern.slf4j.Slf4j; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ConcurrentReferenceHashMap; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.transport.TransportService; -import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; -import org.thingsboard.server.common.transport.adaptor.ProtoConverter; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; -import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; -import org.thingsboard.server.gen.transport.TransportApiProtos; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; -import org.thingsboard.server.transport.mqtt.MqttTransportContext; -import org.thingsboard.server.transport.mqtt.MqttTransportHandler; -import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; -import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; -import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; -import org.thingsboard.server.transport.mqtt.util.ReturnCode; -import javax.annotation.Nullable; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; -import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; -import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; -import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; -import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_RPC_ASYNC_MSG; /** - * Created by ashvayka on 19.01.17. + * Created by nickAS21 on 26.12.22 */ -@Slf4j -public class GatewaySessionHandler { - - private static final String DEFAULT_DEVICE_TYPE = "default"; - private static final String CAN_T_PARSE_VALUE = "Can't parse value: "; - private static final String DEVICE_PROPERTY = "device"; - - private final MqttTransportContext context; - private final TransportService transportService; - private final TransportDeviceInfo gateway; - private final UUID sessionId; - private final ConcurrentMap deviceCreationLockMap; - private final ConcurrentMap devices; - private final ConcurrentMap> deviceFutures; - private final ConcurrentMap mqttQoSMap; - private final ChannelHandlerContext channel; - private final DeviceSessionCtx deviceSessionCtx; +public class GatewaySessionHandler extends AbstractGatewaySessionHandler { public GatewaySessionHandler(DeviceSessionCtx deviceSessionCtx, UUID sessionId) { - this.context = deviceSessionCtx.getContext(); - this.transportService = context.getTransportService(); - this.deviceSessionCtx = deviceSessionCtx; - this.gateway = deviceSessionCtx.getDeviceInfo(); - this.sessionId = sessionId; - this.devices = new ConcurrentHashMap<>(); - this.deviceFutures = new ConcurrentHashMap<>(); - this.deviceCreationLockMap = createWeakMap(); - this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap(); - this.channel = deviceSessionCtx.getChannel(); - } - - ConcurrentReferenceHashMap createWeakMap() { - return new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK); + super(deviceSessionCtx, sessionId); } public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException { @@ -119,14 +39,6 @@ public class GatewaySessionHandler { } } - public void onDeviceDisconnect(MqttPublishMessage mqttMsg) throws AdaptorException { - if (isJsonPayloadType()) { - onDeviceDisconnectJson(mqttMsg); - } else { - onDeviceDisconnectProto(mqttMsg); - } - } - public void onDeviceTelemetry(MqttPublishMessage mqttMsg) throws AdaptorException { int msgId = getMsgId(mqttMsg); ByteBuf payload = mqttMsg.payload(); @@ -137,614 +49,9 @@ public class GatewaySessionHandler { } } - public void onDeviceClaim(MqttPublishMessage mqttMsg) throws AdaptorException { - int msgId = getMsgId(mqttMsg); - ByteBuf payload = mqttMsg.payload(); - if (isJsonPayloadType()) { - onDeviceClaimJson(msgId, payload); - } else { - onDeviceClaimProto(msgId, payload); - } - } - - public void onDeviceAttributes(MqttPublishMessage mqttMsg) throws AdaptorException { - int msgId = getMsgId(mqttMsg); - ByteBuf payload = mqttMsg.payload(); - if (isJsonPayloadType()) { - onDeviceAttributesJson(msgId, payload); - } else { - onDeviceAttributesProto(msgId, payload); - } - } - - public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException { - if (isJsonPayloadType()) { - onDeviceAttributesRequestJson(mqttMsg); - } else { - onDeviceAttributesRequestProto(mqttMsg); - } - } - - public void onDeviceRpcResponse(MqttPublishMessage mqttMsg) throws AdaptorException { - int msgId = getMsgId(mqttMsg); - ByteBuf payload = mqttMsg.payload(); - if (isJsonPayloadType()) { - onDeviceRpcResponseJson(msgId, payload); - } else { - onDeviceRpcResponseProto(msgId, payload); - } - } - - public void onGatewayDisconnect() { - devices.forEach(this::deregisterSession); - } - - public void onDeviceDeleted(String deviceName) { - deregisterSession(deviceName); - } - - public String getNodeId() { - return context.getNodeId(); - } - - public UUID getSessionId() { - return sessionId; - } - - public MqttTransportAdaptor getPayloadAdaptor() { - return deviceSessionCtx.getPayloadAdaptor(); - } - - void deregisterSession(String deviceName) { - GatewayDeviceSessionCtx deviceSessionCtx = devices.remove(deviceName); - if (deviceSessionCtx != null) { - deregisterSession(deviceName, deviceSessionCtx); - } else { - log.debug("[{}] Device [{}] was already removed from the gateway session", sessionId, deviceName); - } - } - - ChannelFuture writeAndFlush(MqttMessage mqttMessage) { - return channel.writeAndFlush(mqttMessage); - } - - int nextMsgId() { - return deviceSessionCtx.nextMsgId(); - } - - private boolean isJsonPayloadType() { - return deviceSessionCtx.isJsonPayloadType(); - } - - private void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) { - log.trace("[{}] onDeviceConnect: {}", sessionId, deviceName); - Futures.addCallback(onDeviceConnect(deviceName, deviceType), new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx result) { - ack(msg, ReturnCode.SUCCESS); - log.trace("[{}] onDeviceConnectOk: {}", sessionId, deviceName); - } - - @Override - public void onFailure(Throwable t) { - log.warn("[{}] Failed to process device connect command: {}", sessionId, deviceName, t); - - } - }, context.getExecutor()); - } - - private ListenableFuture onDeviceConnect(String deviceName, String deviceType) { - GatewayDeviceSessionCtx result = devices.get(deviceName); - if (result == null) { - Lock deviceCreationLock = deviceCreationLockMap.computeIfAbsent(deviceName, s -> new ReentrantLock()); - deviceCreationLock.lock(); - try { - result = devices.get(deviceName); - if (result == null) { - return getDeviceCreationFuture(deviceName, deviceType); - } else { - return Futures.immediateFuture(result); - } - } finally { - deviceCreationLock.unlock(); - } - } else { - return Futures.immediateFuture(result); - } - } - - private ListenableFuture getDeviceCreationFuture(String deviceName, String deviceType) { - final SettableFuture futureToSet = SettableFuture.create(); - ListenableFuture future = deviceFutures.putIfAbsent(deviceName, futureToSet); - if (future != null) { - return future; - } - try { - transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() - .setDeviceName(deviceName) - .setDeviceType(deviceType) - .setGatewayIdMSB(gateway.getDeviceId().getId().getMostSignificantBits()) - .setGatewayIdLSB(gateway.getDeviceId().getId().getLeastSignificantBits()).build(), - new TransportServiceCallback() { - @Override - public void onSuccess(GetOrCreateDeviceFromGatewayResponse msg) { - GatewayDeviceSessionCtx deviceSessionCtx = new GatewayDeviceSessionCtx(GatewaySessionHandler.this, msg.getDeviceInfo(), msg.getDeviceProfile(), mqttQoSMap, transportService); - if (devices.putIfAbsent(deviceName, deviceSessionCtx) == null) { - log.trace("[{}] First got or created device [{}], type [{}] for the gateway session", sessionId, deviceName, deviceType); - SessionInfoProto deviceSessionInfo = deviceSessionCtx.getSessionInfo(); - transportService.registerAsyncSession(deviceSessionInfo, deviceSessionCtx); - transportService.process(TransportProtos.TransportToDeviceActorMsg.newBuilder() - .setSessionInfo(deviceSessionInfo) - .setSessionEvent(SESSION_EVENT_MSG_OPEN) - .setSubscribeToAttributes(SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG) - .setSubscribeToRPC(SUBSCRIBE_TO_RPC_ASYNC_MSG) - .build(), null); - } - futureToSet.set(devices.get(deviceName)); - deviceFutures.remove(deviceName); - } - - @Override - public void onError(Throwable e) { - log.warn("[{}] Failed to process device connect command: {}", sessionId, deviceName, e); - futureToSet.setException(e); - deviceFutures.remove(deviceName); - } - }); - return futureToSet; - } catch (Throwable e) { - deviceFutures.remove(deviceName); - throw e; - } - } - - private int getMsgId(MqttPublishMessage mqttMsg) { - return mqttMsg.variableHeader().packetId(); - } - - private void onDeviceConnectJson(MqttPublishMessage mqttMsg) throws AdaptorException { - JsonElement json = getJson(mqttMsg); - String deviceName = checkDeviceName(getDeviceName(json)); - String deviceType = getDeviceType(json); - processOnConnect(mqttMsg, deviceName, deviceType); - } - - private void onDeviceConnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { - try { - TransportApiProtos.ConnectMsg connectProto = TransportApiProtos.ConnectMsg.parseFrom(getBytes(mqttMsg.payload())); - String deviceName = checkDeviceName(connectProto.getDeviceName()); - String deviceType = StringUtils.isEmpty(connectProto.getDeviceType()) ? DEFAULT_DEVICE_TYPE : connectProto.getDeviceType(); - processOnConnect(mqttMsg, deviceName, deviceType); - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void onDeviceDisconnectJson(MqttPublishMessage msg) throws AdaptorException { - String deviceName = checkDeviceName(getDeviceName(getJson(msg))); - processOnDisconnect(msg, deviceName); - } - - private void onDeviceDisconnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { - try { - TransportApiProtos.DisconnectMsg connectProto = TransportApiProtos.DisconnectMsg.parseFrom(getBytes(mqttMsg.payload())); - String deviceName = checkDeviceName(connectProto.getDeviceName()); - processOnDisconnect(mqttMsg, deviceName); - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void processOnDisconnect(MqttPublishMessage msg, String deviceName) { - deregisterSession(deviceName); - ack(msg, ReturnCode.SUCCESS); - } - - private void onDeviceTelemetryJson(int msgId, ByteBuf payload) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - for (Map.Entry deviceEntry : jsonObj.entrySet()) { - String deviceName = deviceEntry.getKey(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - if (!deviceEntry.getValue().isJsonArray()) { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - try { - TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray()); - processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); - channel.close(); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - } - - private void onDeviceTelemetryProto(int msgId, ByteBuf payload) throws AdaptorException { - try { - TransportApiProtos.GatewayTelemetryMsg telemetryMsgProto = TransportApiProtos.GatewayTelemetryMsg.parseFrom(getBytes(payload)); - List deviceMsgList = telemetryMsgProto.getMsgList(); - if (!CollectionUtils.isEmpty(deviceMsgList)) { - deviceMsgList.forEach(telemetryMsg -> { - String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - TransportProtos.PostTelemetryMsg msg = telemetryMsg.getMsg(); - try { - TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray()); - processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); - channel.close(); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - }); - } else { - log.debug("[{}] Devices telemetry messages is empty for: [{}]", sessionId, gateway.getDeviceId()); - throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry messages is empty for [" + gateway.getDeviceId() + "]"); - } - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void processPostTelemetryMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.PostTelemetryMsg postTelemetryMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); - } - - private void onDeviceClaimJson(int msgId, ByteBuf payload) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - for (Map.Entry deviceEntry : jsonObj.entrySet()) { - String deviceName = deviceEntry.getKey(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - if (!deviceEntry.getValue().isJsonObject()) { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - try { - DeviceId deviceId = deviceCtx.getDeviceId(); - TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, deviceEntry.getValue()); - processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - } - - private void onDeviceClaimProto(int msgId, ByteBuf payload) throws AdaptorException { - try { - TransportApiProtos.GatewayClaimMsg claimMsgProto = TransportApiProtos.GatewayClaimMsg.parseFrom(getBytes(payload)); - List claimMsgList = claimMsgProto.getMsgList(); - if (!CollectionUtils.isEmpty(claimMsgList)) { - claimMsgList.forEach(claimDeviceMsg -> { - String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - TransportApiProtos.ClaimDevice claimRequest = claimDeviceMsg.getClaimRequest(); - if (claimRequest == null) { - throw new IllegalArgumentException("Claim request for device: " + deviceName + " is null!"); - } - try { - DeviceId deviceId = deviceCtx.getDeviceId(); - TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray()); - processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, claimRequest, e); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - }); - } else { - log.debug("[{}] Devices claim messages is empty for: [{}]", sessionId, gateway.getDeviceId()); - throw new IllegalArgumentException("[" + sessionId + "] Devices claim messages is empty for [" + gateway.getDeviceId() + "]"); - } - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void processClaimDeviceMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.ClaimDeviceMsg claimDeviceMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); - } - - private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - for (Map.Entry deviceEntry : jsonObj.entrySet()) { - String deviceName = deviceEntry.getKey(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - if (!deviceEntry.getValue().isJsonObject()) { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject()); - processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - } - - private void onDeviceAttributesProto(int msgId, ByteBuf payload) throws AdaptorException { - try { - TransportApiProtos.GatewayAttributesMsg attributesMsgProto = TransportApiProtos.GatewayAttributesMsg.parseFrom(getBytes(payload)); - List attributesMsgList = attributesMsgProto.getMsgList(); - if (!CollectionUtils.isEmpty(attributesMsgList)) { - attributesMsgList.forEach(attributesMsg -> { - String deviceName = checkDeviceName(attributesMsg.getDeviceName()); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); - if (kvListProto == null) { - throw new IllegalArgumentException("Attributes List for device: " + deviceName + " is empty!"); - } - try { - TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); - processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - }); - } else { - log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); - throw new IllegalArgumentException("[" + sessionId + "] Devices attributes keys list is empty for [" + gateway.getDeviceId() + "]"); - } - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void processPostAttributesMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.PostAttributeMsg postAttributeMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); - } - - private void onDeviceAttributesRequestJson(MqttPublishMessage msg) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, msg.payload()); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - int requestId = jsonObj.get("id").getAsInt(); - String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); - boolean clientScope = jsonObj.get("client").getAsBoolean(); - Set keys; - if (jsonObj.has("key")) { - keys = Collections.singleton(jsonObj.get("key").getAsString()); - } else { - JsonArray keysArray = jsonObj.get("keys").getAsJsonArray(); - keys = new HashSet<>(); - for (JsonElement keyObj : keysArray) { - keys.add(keyObj.getAsString()); - } - } - TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); - processGetAttributeRequestMessage(msg, deviceName, requestMsg); - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - } - - private void onDeviceAttributesRequestProto(MqttPublishMessage mqttMsg) throws AdaptorException { - try { - TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = TransportApiProtos.GatewayAttributesRequestMsg.parseFrom(getBytes(mqttMsg.payload())); - String deviceName = checkDeviceName(gatewayAttributesRequestMsg.getDeviceName()); - int requestId = gatewayAttributesRequestMsg.getId(); - boolean clientScope = gatewayAttributesRequestMsg.getClient(); - ProtocolStringList keysList = gatewayAttributesRequestMsg.getKeysList(); - Set keys = new HashSet<>(keysList); - TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); - processGetAttributeRequestMessage(mqttMsg, deviceName, requestMsg); - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void onDeviceRpcResponseJson(int msgId, ByteBuf payload) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - Integer requestId = jsonObj.get("id").getAsInt(); - String data = jsonObj.get("data").toString(); - TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setRequestId(requestId).setPayload(data).build(); - processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } + @Override + protected GatewayDeviceSessionContext newDeviceSessionCtx(GetOrCreateDeviceFromGatewayResponse msg) { + return new GatewayDeviceSessionContext(this, msg.getDeviceInfo(), msg.getDeviceProfile(), mqttQoSMap, transportService); } - private void onDeviceRpcResponseProto(int msgId, ByteBuf payload) throws AdaptorException { - try { - TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.parseFrom(getBytes(payload)); - String deviceName = checkDeviceName(gatewayRpcResponseMsg.getDeviceName()); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - Integer requestId = gatewayRpcResponseMsg.getId(); - String data = gatewayRpcResponseMsg.getData(); - TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setRequestId(requestId).setPayload(data).build(); - processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void processRpcResponseMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); - } - - private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) { - int msgId = getMsgId(mqttMsg); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); - } - - @Override - public void onFailure(Throwable t) { - ack(mqttMsg, ReturnCode.IMPLEMENTATION_SPECIFIC); - log.debug("[{}] Failed to process device attributes request command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } - - private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set keys) { - TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); - result.setRequestId(requestId); - - if (clientScope) { - result.addAllClientAttributeNames(keys); - } else { - result.addAllSharedAttributeNames(keys); - } - return result.build(); - } - - private ListenableFuture checkDeviceConnected(String deviceName) { - GatewayDeviceSessionCtx ctx = devices.get(deviceName); - if (ctx == null) { - log.debug("[{}] Missing device [{}] for the gateway session", sessionId, deviceName); - return onDeviceConnect(deviceName, DEFAULT_DEVICE_TYPE); - } else { - return Futures.immediateFuture(ctx); - } - } - - private String checkDeviceName(String deviceName) { - if (StringUtils.isEmpty(deviceName)) { - throw new RuntimeException("Device name is empty!"); - } else { - return deviceName; - } - } - - private String getDeviceName(JsonElement json) { - return json.getAsJsonObject().get(DEVICE_PROPERTY).getAsString(); - } - - private String getDeviceType(JsonElement json) { - JsonElement type = json.getAsJsonObject().get("type"); - return type == null || type instanceof JsonNull ? DEFAULT_DEVICE_TYPE : type.getAsString(); - } - - private JsonElement getJson(MqttPublishMessage mqttMsg) throws AdaptorException { - return JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); - } - - private byte[] getBytes(ByteBuf payload) { - return ProtoMqttAdaptor.toBytes(payload); - } - - private void ack(MqttPublishMessage msg, ReturnCode returnCode) { - int msgId = getMsgId(msg); - if (msgId > 0) { - writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, returnCode)); - } - } - - private void deregisterSession(String deviceName, GatewayDeviceSessionCtx deviceSessionCtx) { - transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); - transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_CLOSED, null); - log.debug("[{}] Removed device [{}] from the gateway session", sessionId, deviceName); - } - - private TransportServiceCallback getPubAckCallback(final ChannelHandlerContext ctx, final String deviceName, final int msgId, final T msg) { - return new TransportServiceCallback() { - @Override - public void onSuccess(Void dummy) { - log.trace("[{}][{}] Published msg: {}", sessionId, deviceName, msg); - if (msgId > 0) { - ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, ReturnCode.SUCCESS)); - } - } - - @Override - public void onError(Throwable e) { - log.trace("[{}] Failed to publish msg: {} for device: {}", sessionId, msg, deviceName, e); - ctx.close(); - } - }; - } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java index 4068604b19..0498eadd9f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java @@ -16,18 +16,13 @@ package org.thingsboard.server.transport.mqtt.session; import io.netty.handler.codec.mqtt.MqttQoS; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.DeviceTransportType; -import org.thingsboard.server.common.data.TransportPayloadType; -import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; -import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext; -import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter; -import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java new file mode 100644 index 0000000000..fb4ab488a8 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.session; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugRpcRequestHeader; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic; + +import java.util.Date; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.getTsKvProto; + +@Slf4j +public class SparkplugDeviceSessionContext extends AbstractGatewayDeviceSessionContext { + + private final Map deviceBirthMetrics = new ConcurrentHashMap<>(); + + public SparkplugDeviceSessionContext(SparkplugNodeSessionHandler parent, + TransportDeviceInfo deviceInfo, + DeviceProfile deviceProfile, + ConcurrentMap mqttQoSMap, + TransportService transportService) { + super(parent, deviceInfo, deviceProfile, mqttQoSMap, transportService); + } + + public Map getDeviceBirthMetrics() { + return deviceBirthMetrics; + } + + public void setDeviceBirthMetrics(java.util.List metrics) { + this.deviceBirthMetrics.putAll(metrics.stream() + .collect(Collectors.toMap(SparkplugBProto.Payload.Metric::getName, metric -> metric))); + } + + + @Override + public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg notification) { + log.trace("[{}] Received attributes update notification to sparkplug device", sessionId); + notification.getSharedUpdatedList().forEach(tsKvProto -> { + if (getDeviceBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { + SparkplugTopic sparkplugTopic = new SparkplugTopic(parent.getSparkplugTopicNode(), + SparkplugMessageType.DCMD, deviceInfo.getDeviceName()); + parent.createSparkplugMqttPublishMsg(tsKvProto, + sparkplugTopic.toString(), + getDeviceBirthMetrics().get(tsKvProto.getKv().getKey())) + .ifPresent(this.parent::writeAndFlush); + } + }); + } + + @Override + public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { + log.trace("[{}] Received RPC Request notification to sparkplug device", sessionId); + try { + SparkplugMessageType messageType = SparkplugMessageType.parseMessageType(rpcRequest.getMethodName()); + SparkplugRpcRequestHeader header = JacksonUtil.fromString(rpcRequest.getParams(), SparkplugRpcRequestHeader.class); + header.setMessageType(messageType.name()); + TransportProtos.TsKvProto tsKvProto = getTsKvProto(header.getMetricName(), header.getValue(), new Date().getTime()); + if (getDeviceBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { + SparkplugTopic sparkplugTopic = new SparkplugTopic(parent.getSparkplugTopicNode(), + messageType, deviceInfo.getDeviceName()); + parent.createSparkplugMqttPublishMsg(tsKvProto, + sparkplugTopic.toString(), + getDeviceBirthMetrics().get(tsKvProto.getKv().getKey())) + .ifPresent(payload -> parent.sendToDeviceRpcRequest(payload, rpcRequest, sessionInfo)); + } else { + parent.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), + ThingsboardErrorCode.BAD_REQUEST_PARAMS, " Failed send To Device Rpc Request: " + + rpcRequest.getMethodName() + ". This device does not have a metricName: [" + tsKvProto.getKv().getKey() + "]"); + } + } catch (ThingsboardException e) { + parent.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), + ThingsboardErrorCode.BAD_REQUEST_PARAMS, " Failed send To Device Rpc Request: " + + rpcRequest.getMethodName() + ". " + e.getMessage()); + } + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java new file mode 100644 index 0000000000..fc480fa882 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -0,0 +1,340 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.session; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.protobuf.Descriptors; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttPublishMessage; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.netty.handler.codec.mqtt.MqttTopicSubscription; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.ResponseCode; +import org.springframework.util.CollectionUtils; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.adaptor.ProtoConverter; +import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; +import org.thingsboard.server.transport.mqtt.MqttTransportHandler; +import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DBIRTH; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NBIRTH; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.fromSparkplugBMetricToKeyValueProto; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.validatedValueByTypeMetric; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.parseTopicSubscribe; + +/** + * Created by nickAS21 on 12.12.22 + */ +@Slf4j +public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler { + + private final SparkplugTopic sparkplugTopicNode; + private final Map nodeBirthMetrics; + private final MqttTransportHandler parent; + + public SparkplugNodeSessionHandler(MqttTransportHandler parent, DeviceSessionCtx deviceSessionCtx, UUID sessionId, + SparkplugTopic sparkplugTopicNode) { + super(deviceSessionCtx, sessionId); + this.parent = parent; + this.sparkplugTopicNode = sparkplugTopicNode; + this.nodeBirthMetrics = new ConcurrentHashMap<>(); + } + + public void setNodeBirthMetrics(java.util.List metrics) { + this.nodeBirthMetrics.putAll(metrics.stream() + .collect(Collectors.toMap(SparkplugBProto.Payload.Metric::getName, metric -> metric))); + } + + public Map getNodeBirthMetrics() { + return this.nodeBirthMetrics; + } + + public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; + byte[] bytes = getBytes(inbound.payload()); + Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); + try { + return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); + } catch (Exception e) { + log.debug("Failed to decode post telemetry request", e); + throw new AdaptorException(e); + } + } + + public void onAttributesTelemetryProto(int msgId, SparkplugBProto.Payload sparkplugBProto, String deviceName, SparkplugTopic topic) throws AdaptorException, ThingsboardException { + checkDeviceName(deviceName); + + ListenableFuture contextListenableFuture; + if (topic.isNode()) { + if (topic.isType(NBIRTH)) { + sendSparkplugStateOnTelemetry(this.deviceSessionCtx.getSessionInfo(), deviceName, ONLINE, + sparkplugBProto.getTimestamp()); + setNodeBirthMetrics(sparkplugBProto.getMetricsList()); + } + contextListenableFuture = Futures.immediateFuture(this.deviceSessionCtx); + } else { + ListenableFuture deviceCtx = onDeviceConnectProto(deviceName); + contextListenableFuture = Futures.transform(deviceCtx, ctx -> { + if (topic.isType(DBIRTH)) { + sendSparkplugStateOnTelemetry(ctx.getSessionInfo(), deviceName, ONLINE, + sparkplugBProto.getTimestamp()); + ctx.setDeviceBirthMetrics(sparkplugBProto.getMetricsList()); + } + return ctx; + }, MoreExecutors.directExecutor()); + } + Set attributesMetricNames = ((MqttDeviceProfileTransportConfiguration) deviceSessionCtx + .getDeviceProfile().getProfileData().getTransportConfiguration()).getSparkplugAttributesMetricNames(); + if (attributesMetricNames != null) { + List attributesMsgList = convertToPostAttributes(sparkplugBProto, attributesMetricNames, deviceName); + onDeviceAttributesProto(contextListenableFuture, msgId, attributesMsgList, deviceName); + } + List postTelemetryMsgList = convertToPostTelemetry(sparkplugBProto, attributesMetricNames, topic.getType().name()); + onDeviceTelemetryProto(contextListenableFuture, msgId, postTelemetryMsgList, deviceName); + } + + public void onDeviceTelemetryProto(ListenableFuture contextListenableFuture, + int msgId, List postTelemetryMsgList, String deviceName) throws AdaptorException { + try { + int finalMsgId = msgId; + postTelemetryMsgList.forEach(telemetryMsg -> { + Futures.addCallback(contextListenableFuture, + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable MqttDeviceAwareSessionContext deviceCtx) { + try { + processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, finalMsgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, telemetryMsg, e); + channel.close(); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } catch (RuntimeException e) { + throw new AdaptorException(e); + } + } + + private void onDeviceAttributesProto(ListenableFuture contextListenableFuture, int msgId, + List attributesMsgList, String deviceName) throws AdaptorException { + try { + if (!CollectionUtils.isEmpty(attributesMsgList)) { + attributesMsgList.forEach(attributesMsg -> { + Futures.addCallback(contextListenableFuture, + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable MqttDeviceAwareSessionContext deviceCtx) { + TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); + try { + TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); + } + } catch (RuntimeException e) { + throw new AdaptorException(e); + } + } + + public void handleSparkplugSubscribeMsg(List grantedQoSList, MqttTopicSubscription subscription, + MqttQoS reqQoS) throws ThingsboardException, AdaptorException, + ExecutionException, InterruptedException { + SparkplugTopic sparkplugTopic = parseTopicSubscribe(subscription.topicName()); + if (sparkplugTopic.getGroupId() == null) { + // TODO SUBSCRIBE NameSpace + } else if (sparkplugTopic.getType() == null) { + // TODO SUBSCRIBE GroupId + } else if (sparkplugTopic.isNode()) { + // SUBSCRIBE Node + parent.processAttributesRpcSubscribeSparkplugNode(grantedQoSList, reqQoS); + } else { + // SUBSCRIBE Device - DO NOTHING, WE HAVE ALREADY SUBSCRIBED. + // TODO: track that node subscribed to # or to particular device. + } + } + + public void onDeviceDisconnect(MqttPublishMessage mqttMsg, String deviceName) throws AdaptorException { + try { + processOnDisconnect(mqttMsg, deviceName); + } catch (RuntimeException e) { + throw new AdaptorException(e); + } + } + + private ListenableFuture onDeviceConnectProto(String deviceName) throws ThingsboardException { + try { + String deviceType = this.gateway.getDeviceType() + "-node"; + return onDeviceConnect(deviceName, deviceType); + } catch (RuntimeException e) { + log.error("Failed Sparkplug Device connect proto!", e); + throw new ThingsboardException(e, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } + + private List convertToPostTelemetry(SparkplugBProto.Payload sparkplugBProto, Set attributesMetricNames, String topicTypeName) throws AdaptorException { + try { + List msgs = new ArrayList<>(); + for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) { + if (attributesMetricNames == null || !attributesMetricNames.contains(protoMetric.getName())) { + long ts = protoMetric.getTimestamp(); + String key = "bdSeq".equals(protoMetric.getName()) ? + topicTypeName + " " + protoMetric.getName() : protoMetric.getName(); + Optional keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(key, protoMetric); + if (keyValueProtoOpt.isPresent()) { + msgs.add(postTelemetryMsgCreated(keyValueProtoOpt.get(), ts)); + } + } + } + + if (DBIRTH.name().equals(topicTypeName)) { + TransportProtos.KeyValueProto.Builder keyValueProtoBuilder = TransportProtos.KeyValueProto.newBuilder(); + keyValueProtoBuilder.setKey(topicTypeName + " " + "seq"); + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.LONG_V); + keyValueProtoBuilder.setLongV(sparkplugBProto.getSeq()); + msgs.add(postTelemetryMsgCreated(keyValueProtoBuilder.build(), sparkplugBProto.getTimestamp())); + } + return msgs; + } catch (IllegalStateException | JsonSyntaxException | ThingsboardException e) { + log.error("Failed to decode post telemetry request", e); + throw new AdaptorException(e); + } + } + + private List convertToPostAttributes(SparkplugBProto.Payload sparkplugBProto, + Set attributesMetricNames, + String deviceName) throws AdaptorException { + try { + List msgs = new ArrayList<>(); + for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) { + if (attributesMetricNames.contains(protoMetric.getName())) { + TransportApiProtos.AttributesMsg.Builder deviceAttributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); + Optional msgOpt = getPostAttributeMsg(protoMetric); + if (msgOpt.isPresent()) { + deviceAttributesMsgBuilder.setDeviceName(deviceName); + deviceAttributesMsgBuilder.setMsg(msgOpt.get()); + msgs.add(deviceAttributesMsgBuilder.build()); + } + } + } + return msgs; + } catch (IllegalStateException | JsonSyntaxException | ThingsboardException e) { + log.error("Failed to decode post telemetry request", e); + throw new AdaptorException(e); + } + } + + private Optional getPostAttributeMsg(SparkplugBProto.Payload.Metric protoMetric) throws ThingsboardException { + Optional keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(protoMetric.getName(), protoMetric); + if (keyValueProtoOpt.isPresent()) { + TransportProtos.PostAttributeMsg.Builder builder = TransportProtos.PostAttributeMsg.newBuilder(); + builder.addKv(keyValueProtoOpt.get()); + return Optional.of(builder.build()); + } + return Optional.empty(); + } + + public SparkplugTopic getSparkplugTopicNode() { + return this.sparkplugTopicNode; + } + + public Optional createSparkplugMqttPublishMsg(TransportProtos.TsKvProto tsKvProto, + String sparkplugTopic, + SparkplugBProto.Payload.Metric metricBirth) { + try { + long ts = tsKvProto.getTs(); + MetricDataType metricDataType = MetricDataType.fromInteger(metricBirth.getDatatype()); + Optional value = validatedValueByTypeMetric(tsKvProto.getKv(), metricDataType); + if (value.isPresent()) { + SparkplugBProto.Payload.Builder cmdPayload = SparkplugBProto.Payload.newBuilder() + .setTimestamp(ts); + cmdPayload.addMetrics(createMetric(value.get(), ts, tsKvProto.getKv().getKey(), metricDataType)); + byte[] payloadInBytes = cmdPayload.build().toByteArray(); + return Optional.of(getPayloadAdaptor().createMqttPublishMsg(deviceSessionCtx, sparkplugTopic, payloadInBytes)); + } else { + log.trace("DeviceId: [{}] tenantId: [{}] sessionId:[{}] Failed to convert device attributes [{}] response to MQTT sparkplug msg", + deviceSessionCtx.getDeviceInfo().getDeviceId(), deviceSessionCtx.getDeviceInfo().getTenantId(), sessionId, tsKvProto.getKv()); + } + } catch (Exception e) { + log.trace("DeviceId: [{}] tenantId: [{}] sessionId:[{}] Failed to convert device attributes response to MQTT sparkplug msg", + deviceSessionCtx.getDeviceInfo().getDeviceId(), deviceSessionCtx.getDeviceInfo().getTenantId(), sessionId, e); + return Optional.empty(); + } + return Optional.empty(); + } + + @Override + protected SparkplugDeviceSessionContext newDeviceSessionCtx(GetOrCreateDeviceFromGatewayResponse msg) { + return new SparkplugDeviceSessionContext(this, msg.getDeviceInfo(), msg.getDeviceProfile(), mqttQoSMap, transportService); + } + + protected void sendToDeviceRpcRequest(MqttMessage payload, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, TransportProtos.SessionInfoProto sessionInfo) { + parent.sendToDeviceRpcRequest(payload, rpcRequest, sessionInfo); + } + + protected void sendErrorRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ThingsboardErrorCode result, String errorMsg) { + parent.sendErrorRpcResponse(sessionInfo, requestId, result, errorMsg); + } + + protected void sendSuccessRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ResponseCode result, String successMsg) { + parent.sendSuccessRpcResponse(sessionInfo, requestId, result, successMsg); + } + + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/MetricDataType.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/MetricDataType.java new file mode 100644 index 0000000000..d5e781a402 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/MetricDataType.java @@ -0,0 +1,156 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; + +import java.math.BigInteger; +import java.util.Date; + +/** + * Created by nickAS21 on 10.01.23 + */ + +@Slf4j +public enum MetricDataType { + + // Basic Types + Int8(1, Byte.class), + Int16(2, Short.class), + Int32(3, Integer.class), + Int64(4, Long.class), + UInt8(5, Short.class), + UInt16(6, Integer.class), + UInt32(7, Long.class), + UInt64(8, BigInteger.class), + Float(9, Float.class), + Double(10, Double.class), + Boolean(11, Boolean.class), + String(12, String.class), + DateTime(13, Date.class), + Text(14, String.class), + + // Custom Types for Metrics + UUID(15, String.class), + DataSet(16, SparkplugBProto.Payload.DataSet.class), + Bytes(17, byte[].class), + File(18, SparkplugMetricUtil.File.class), + Template(19, SparkplugBProto.Payload.Template.class), + + // PropertyValue Types (20 and 21) are NOT metric datatypes + + // Unknown + Unknown(0, Object.class); + + private Class clazz = null; + private int intValue = 0; + + /** + * Constructor + * + * @param intValue the integer value of this {@link MetricDataType} + * @param clazz the {@link Class} type associated with this {@link MetricDataType} + */ + private MetricDataType(int intValue, Class clazz) { + this.intValue = intValue; + this.clazz = clazz; + } + + /** + * Checks the type of a specified value against the specified {@link MetricDataType} + * + * @param value the {@link Object} value to check against the {@link MetricDataType} + * @throws AdaptorException if the value is not a valid type for the given {@link MetricDataType} + */ + public void checkType(Object value) throws AdaptorException { + if (value != null && !clazz.isAssignableFrom(value.getClass())) { + String msgError = "Failed type check - " + clazz + " != " + ((value != null) ? value.getClass().toString() : "null"); + log.debug(msgError); + throw new AdaptorException(msgError); + } + } + + /** + * Returns an integer representation of the data type. + * + * @return an integer representation of the data type. + */ + public int toIntValue() { + return this.intValue; + } + + /** + * Converts the integer representation of the data type into a {@link MetricDataType} instance. + * + * @param i the integer representation of the data type. + * @return a {@link MetricDataType} instance. + */ + public static MetricDataType fromInteger(int i) { + switch (i) { + case 1: + return Int8; + case 2: + return Int16; + case 3: + return Int32; + case 4: + return Int64; + case 5: + return UInt8; + case 6: + return UInt16; + case 7: + return UInt32; + case 8: + return UInt64; + case 9: + return Float; + case 10: + return Double; + case 11: + return Boolean; + case 12: + return String; + case 13: + return DateTime; + case 14: + return Text; + case 15: + return UUID; + case 16: + return DataSet; + case 17: + return Bytes; + case 18: + return File; + case 19: + return Template; + default: + return Unknown; + } + } + + /** + * @return the class type for this DataType + */ + public Class getClazz() { + return clazz; + } + + +} \ No newline at end of file diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugConnectionState.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugConnectionState.java new file mode 100644 index 0000000000..646dc1842e --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugConnectionState.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; + +public enum SparkplugConnectionState { + /** + * The EoN node should examine the payload of this + * message to ensure that it is a value of “ONLINE” + */ + OFFLINE, + /** + * If the value is “OFFLINE”, this indicates the Primary Application + * has lost its MQTT Session to this particular MQTT Server. + */ + ONLINE + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMessageType.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMessageType.java new file mode 100644 index 0000000000..f9ca6299bc --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMessageType.java @@ -0,0 +1,113 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; + +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; + +/** + * An enumeration of Sparkplug MQTT message types. The type provides an indication as to what the MQTT Payload of + * message will contain. + */ +public enum SparkplugMessageType { + + /** + * Birth certificate for MQTT Edge of Network (EoN) Nodes. + */ + NBIRTH, + + /** + * Death certificate for MQTT Edge of Network (EoN) Nodes. + */ + NDEATH, + + /** + * Birth certificate for MQTT Devices. + */ + DBIRTH, + + /** + * Death certificate for MQTT Devices. + */ + DDEATH, + + /** + * Edge of Network (EoN) Node data message. + */ + NDATA, + + /** + * Device data message. + */ + DDATA, + + /** + * Edge of Network (EoN) Node command message. + */ + NCMD, + + /** + * Device command message. + */ + DCMD, + + /** + * Critical application state message. + */ + STATE, + + /** + * Device record message. + */ + DRECORD, + + /** + * Edge of Network (EoN) Node record message. + */ + NRECORD; + + public static SparkplugMessageType parseMessageType(String type) throws ThingsboardException { + for (SparkplugMessageType messageType : SparkplugMessageType.values()) { + if (messageType.name().equals(type)) { + return messageType; + } + } + throw new ThingsboardException("Invalid message type: " + type, ThingsboardErrorCode.INVALID_ARGUMENTS); + } + public static String messageName(SparkplugMessageType type) { + return STATE.equals(type) ? "sparkplugConnectionState" : type.name(); + } + + public boolean isDeath() { + return this.equals(DDEATH) || this.equals(NDEATH); + } + + public boolean isCommand() { + return this.equals(DCMD) || this.equals(NCMD); + } + + public boolean isData() { + return this.equals(DDATA) || this.equals(NDATA); + } + + public boolean isBirth() { + return this.equals(DBIRTH) || this.equals(NBIRTH); + } + + public boolean isRecord() { + return this.equals(DRECORD) || this.equals(NRECORD); + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMetricUtil.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMetricUtil.java new file mode 100644 index 0000000000..65bf6b2063 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMetricUtil.java @@ -0,0 +1,452 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.ser.std.FileSerializer; +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.text.NumberFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.thingsboard.common.util.JacksonUtil.newArrayNode; + +/** + * Provides utility methods for SparkplugB MQTT Payload Metric. + */ +@Slf4j +public class SparkplugMetricUtil { + + public static Optional fromSparkplugBMetricToKeyValueProto(String key, SparkplugBProto.Payload.Metric protoMetric) throws ThingsboardException { + // Check if the null flag has been set indicating that the value is null + if (protoMetric.getIsNull()) { + return Optional.empty(); + } + // Otherwise convert the value based on the type + int metricType = protoMetric.getDatatype(); + TransportProtos.KeyValueProto.Builder builderProto = TransportProtos.KeyValueProto.newBuilder(); + ArrayNode nodeArray = newArrayNode(); + MetricDataType metricDataType = MetricDataType.fromInteger(metricType); + try { + switch (metricDataType) { + case Boolean: + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.BOOLEAN_V) + .setBoolV(protoMetric.getBooleanValue()).build()); + case DateTime: + case Int64: + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) + .setLongV(protoMetric.getLongValue()).build()); + case Float: + var f = new BigDecimal(String.valueOf(protoMetric.getFloatValue())); + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.DOUBLE_V) + .setDoubleV(f.doubleValue()).build()); + case Double: + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) + .setLongV(Double.valueOf(protoMetric.getDoubleValue()).longValue()).build()); + case Int8: + case UInt8: + case Int16: + case Int32: + case UInt16: + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) + .setLongV(protoMetric.getIntValue()).build()); + case UInt32: + case UInt64: + if (protoMetric.hasIntValue()) { + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) + .setLongV(protoMetric.getIntValue()).build()); + } else if (protoMetric.hasLongValue()) { + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) + .setLongV(protoMetric.getLongValue()).build()); + } else { + log.error("Invalid value for UInt32 datatype"); + throw new ThingsboardException("Invalid value for " + MetricDataType.fromInteger(metricType).name() + " datatype " + metricType, ThingsboardErrorCode.INVALID_ARGUMENTS); + } + case String: + case Text: + case UUID: + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.STRING_V) + .setStringV(protoMetric.getStringValue()).build()); + // byte[] + case Bytes: + ByteBuffer byteBuffer = ByteBuffer.wrap(protoMetric.getBytesValue().toByteArray()); + while (byteBuffer.hasRemaining()) { + nodeArray.add(byteBuffer.get()); + } + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.JSON_V) + .setJsonV(nodeArray.toString()).build()); + case DataSet: + case Template: + case File: + //TODO + // Build the and create the DataSet + /** + SparkplugBProto.Payload.DataSet protoDataSet = protoMetric.getDatasetValue(); + return new SparkplugBProto.Payload.DataSet.Builder(protoDataSet.getNumOfColumns()).addColumnNames(protoDataSet.getColumnsList()) + .addTypes(convertDataSetDataTypes(protoDataSet.getTypesList())) + .addRows(convertDataSetRows(protoDataSet.getRowsList(), protoDataSet.getTypesList())) + .createDataSet(); + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.STRING_V) + .setStringV(protoDataSet.toString()).build()); + **/ + //TODO + // Build the and create the Template + /** + SparkplugBProto.Payload.Template protoTemplate = protoMetric.getTemplateValue(); + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.STRING_V) + .setStringV( protoTemplate.toString()).build()); + **/ + //TODO + // Build the and create the File + /** + String filename = protoMetric.getMetadata().getFileName(); + return Optional.of(builderPrbyteValueoto.setKey(key + "_" + filename).setType(TransportProtos.KeyValueType.STRING_V) + .setStringV(Hex.encodeHexString((protoMetric.getBytesValue().toByteArray()))).build()); + **/ + return Optional.empty(); + case Unknown: + default: + throw new ThingsboardException("Failed to decode: Unknown MetricDataType " + metricType, ThingsboardErrorCode.INVALID_ARGUMENTS); + } + } catch (Exception e) { + log.error("", e); + return Optional.empty(); + } + } + + public static SparkplugBProto.Payload.Metric createMetric(Object value, long ts, String key, MetricDataType metricDataType) throws ThingsboardException { + SparkplugBProto.Payload.Metric metric = SparkplugBProto.Payload.Metric.newBuilder() + .setTimestamp(ts) + .setName(key) + .setDatatype(metricDataType.toIntValue()) + .build(); + switch (metricDataType) { + case Int8: // (byte) + return metric.toBuilder().setIntValue(((Byte) value).intValue()).build(); + case Int16: // (short) + case UInt8: + return metric.toBuilder().setIntValue(((Short) value).intValue()).build(); + case UInt16: // (int) + case Int32: + return metric.toBuilder().setIntValue(((Integer) value).intValue()).build(); + case UInt32: // (long) + case Int64: + case UInt64: + case DateTime: + return metric.toBuilder().setLongValue(((Long) value).longValue()).build(); + case Float: // (float) + return metric.toBuilder().setFloatValue(((Float) value).floatValue()).build(); + case Double: // (double) + return metric.toBuilder().setDoubleValue(((Double) value).doubleValue()).build(); + case Boolean: // (boolean) + return metric.toBuilder().setBooleanValue(((Boolean) value).booleanValue()).build(); + case String: // String) + case Text: + case UUID: + return metric.toBuilder().setStringValue((String) value).build(); + case Bytes: + ByteString byteString = ByteString.copyFrom((byte[]) value); + return metric.toBuilder().setBytesValue(byteString).build(); + case DataSet: + return metric.toBuilder().setDatasetValue((SparkplugBProto.Payload.DataSet) value).build(); + case File: + SparkplugMetricUtil.File file = (SparkplugMetricUtil.File) value; + ByteString byteFileString = ByteString.copyFrom(file.getBytes()); + return metric.toBuilder().setBytesValue(byteFileString).build(); + case Template: + return metric.toBuilder().setTemplateValue((SparkplugBProto.Payload.Template) value).build(); + case Unknown: + throw new ThingsboardException("Invalid value for MetricDataType " + metricDataType.name(), ThingsboardErrorCode.INVALID_ARGUMENTS); + } + return metric; + } + + public static TransportProtos.TsKvProto getTsKvProto(String key, Object value, long ts) throws ThingsboardException { + try { + TransportProtos.TsKvProto.Builder tsKvProtoBuilder = TransportProtos.TsKvProto.newBuilder(); + TransportProtos.KeyValueProto.Builder keyValueProtoBuilder = TransportProtos.KeyValueProto.newBuilder(); + keyValueProtoBuilder.setKey(key); + if (value instanceof String) { + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.STRING_V); + keyValueProtoBuilder.setStringV((String) value); + } else if (value instanceof Integer) { + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.LONG_V); + keyValueProtoBuilder.setLongV((Integer) value); + } else if (value instanceof Long) { + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.LONG_V); + keyValueProtoBuilder.setLongV((Long) value); + } else if (value instanceof Boolean) { + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.BOOLEAN_V); + keyValueProtoBuilder.setBoolV((Boolean) value); + } else if (value instanceof Double) { + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.DOUBLE_V); + keyValueProtoBuilder.setDoubleV((Double) value); + } else if (value instanceof List) { + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.JSON_V); + ArrayNode arrayNodeBytes = JacksonUtil.convertValue(value, ArrayNode.class); + keyValueProtoBuilder.setJsonV(arrayNodeBytes.toString()); + } else { + throw new ThingsboardException("Failed to convert device/node RPC command to TsKvProto for Sparkplug MQT msg: value [" + value + "]", ThingsboardErrorCode.INVALID_ARGUMENTS); + } + tsKvProtoBuilder.setKv(keyValueProtoBuilder.build()); + tsKvProtoBuilder.setTs(ts); + return tsKvProtoBuilder.build(); + } catch (Exception e) { + throw new ThingsboardException("Failed to convert device/node RPC command to TsKvProto for Sparkplug MQT msg: value [" + value + "]", ThingsboardErrorCode.INVALID_ARGUMENTS); + } + } + + public static Optional validatedValueByTypeMetric(TransportProtos.KeyValueProto kv, MetricDataType metricDataType) throws ThingsboardException { + if (kv.getTypeValue() <= 3) { + return validatedValuePrimitiveByTypeMetric(kv, metricDataType); + } else if (kv.getTypeValue() == 4) { + JsonNode arrayNode = JacksonUtil.fromString(kv.getJsonV(), JsonNode.class); + if (arrayNode.isArray()) { + return validatedValueJsonByTypeMetric(kv.getJsonV(), metricDataType); + } + } else { + throw new ThingsboardException("Invalid type KeyValueProto " + kv.toString() + " for MetricDataType " + metricDataType.name(), ThingsboardErrorCode.INVALID_ARGUMENTS); + } + return Optional.empty(); + } + + public static Optional validatedValuePrimitiveByTypeMetric(TransportProtos.KeyValueProto kv, MetricDataType metricDataType) throws ThingsboardException { + Optional valueOpt = getValueKvProtoPrimitive(kv); + if (valueOpt.isPresent()) { + try { + switch (metricDataType) { + // int + case Int8: + case Int16: + case UInt8: + case UInt16: + case Int32: + Optional boolInt8 = booleanStringToInt(valueOpt.get()); + if (boolInt8.isPresent()) { + return Optional.of(boolInt8.get()); + } + try { + return Optional.of(Integer.valueOf(valueOpt.get())); + } catch (NumberFormatException eInt) { + var i = new BigDecimal(valueOpt.get()); + if (i.longValue() <= Integer.MAX_VALUE) { + return Optional.of(i.intValue()); + } + throw new ThingsboardException("Invalid type value " + kv.toString() + " for MetricDataType " + + metricDataType.name(), eInt, ThingsboardErrorCode.INVALID_ARGUMENTS); + } + // long + case UInt32: + case Int64: + case UInt64: + case DateTime: + Optional boolInt64 = booleanStringToInt(valueOpt.get()); + if (boolInt64.isPresent()) { + return Optional.of(Long.valueOf(boolInt64.get())); + } + var l = new BigDecimal(valueOpt.get()); + return Optional.of(l.longValue()); + // float + case Float: + Optional boolFloat = booleanStringToInt(valueOpt.get()); + if (boolFloat.isPresent()) { + var fb = new BigDecimal(boolFloat.get()); + return Optional.of(fb.floatValue()); + } + var f = new BigDecimal(valueOpt.get()); + return Optional.of(f.floatValue()); + // double + case Double: + Optional boolDouble = booleanStringToInt(valueOpt.get()); + if (boolDouble.isPresent()) { + return Optional.of(Double.valueOf(boolDouble.get())); + } + var dd = new BigDecimal(valueOpt.get()); + return Optional.of(dd.doubleValue()); + case Boolean: + if ("true".equals(valueOpt.get())) { + return Optional.of(true); + } else if ("false".equals(valueOpt.get())) { + return Optional.of(false); + } else { + Number number = NumberFormat.getInstance().parse(valueOpt.get()); + if (StringUtils.isBlank(number.toString()) || "0".equals(number.toString())) { // ok 0 + return Optional.of(false); + } else { + return Optional.of(true); + } + } + case String: + case Text: + case UUID: + return Optional.of(valueOpt.get()); + } + } catch (Exception e) { + log.trace("Invalid type value [{}] for MetricDataType [{}] [{}]", kv, metricDataType.name(), e.getMessage()); + throw new ThingsboardException("Invalid type value " + kv.toString() + " for MetricDataType " + metricDataType.name(), e, ThingsboardErrorCode.INVALID_ARGUMENTS); + } + } + return Optional.empty(); + } + + public static Optional validatedValueJsonByTypeMetric(String arrayNodeStr, MetricDataType metricDataType) { + try { + Optional valueOpt; + switch (metricDataType) { + // byte[] + case Bytes: + List listBytes = JacksonUtil.fromString(arrayNodeStr, new TypeReference<>() { + }); + byte[] bytes = new byte[listBytes.size()]; + for (int i = 0; i < listBytes.size(); i++) { + bytes[i] = listBytes.get(i).byteValue(); + } + return Optional.of(bytes); + case DataSet: + case File: + case Template: + log.error("Invalid type value [{}] for MetricDataType [{}]", arrayNodeStr, metricDataType.name()); + return Optional.empty(); + case Unknown: + default: + log.error("Invalid MetricDataType [{}] type, value [{}]", arrayNodeStr, metricDataType.name()); + return Optional.empty(); + } + } catch (Exception e) { + log.error("Invalid type value [{}] for MetricDataType [{}] [{}]", arrayNodeStr, metricDataType.name(), e.getMessage()); + return Optional.empty(); + } + } + + private static Optional getValueKvProtoPrimitive(TransportProtos.KeyValueProto kv) { + if (kv.getTypeValue() == 0) { // boolean + return Optional.of(String.valueOf(kv.getBoolV())); + } else if (kv.getTypeValue() == 1) { // kvLong + return Optional.of(String.valueOf(kv.getLongV())); + } else if (kv.getTypeValue() == 2) { // kvDouble/float + return Optional.of(String.valueOf(kv.getDoubleV())); + } else if (kv.getTypeValue() == 3) { // kvString + return Optional.of(kv.getStringV()); + } else { + return Optional.empty(); + } + } + + private static Optional booleanStringToInt(String booleanStr) { + if ("true".equals(booleanStr)) { + return Optional.of(1); + } else if ("false".equals(booleanStr)) { + return Optional.of(0); + } else { + return Optional.empty(); + } + } + + @JsonIgnoreProperties( + value = {"fileName"}) + @JsonSerialize( + using = FileSerializer.class) + public class File { + + private String fileName; + private byte[] bytes; + + /** + * Default Constructor + */ + public File() { + super(); + } + + /** + * Constructor + * + * @param fileName the full file name path + * @param bytes the array of bytes that represent the contents of the file + */ + public File(String fileName, byte[] bytes) { + super(); + this.fileName = fileName == null + ? null + : fileName.replace("/", System.getProperty("file.separator")).replace("\\", + System.getProperty("file.separator")); + this.bytes = Arrays.copyOf(bytes, bytes.length); + } + + /** + * Gets the full filename path + * + * @return the full filename path + */ + public String getFileName() { + return fileName; + } + + /** + * Sets the full filename path + * + * @param fileName the full filename path + */ + public void setFileName(String fileName) { + this.fileName = fileName; + } + + /** + * Gets the bytes that represent the contents of the file + * + * @return the bytes that represent the contents of the file + */ + public byte[] getBytes() { + return bytes; + } + + /** + * Sets the bytes that represent the contents of the file + * + * @param bytes the bytes that represent the contents of the file + */ + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("File [fileName="); + builder.append(fileName); + builder.append(", bytes="); + builder.append(Arrays.toString(bytes)); + builder.append("]"); + return builder.toString(); + } + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugRpcRequestHeader.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugRpcRequestHeader.java new file mode 100644 index 0000000000..1a9ed987f7 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugRpcRequestHeader.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SparkplugRpcRequestHeader { + + private String messageType; + private String metricName; + private Object value; + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugRpcResponseBody.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugRpcResponseBody.java new file mode 100644 index 0000000000..8f39a526c4 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugRpcResponseBody.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SparkplugRpcResponseBody { + + private String result; + private String value; + private String error; + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopic.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopic.java new file mode 100644 index 0000000000..be721f11bb --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopic.java @@ -0,0 +1,164 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Created by nickAS21 on 12.12.22 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SparkplugTopic { + + /** + * The Sparkplug namespace version. + * For the Sparkplug™ B version of the specification, the UTF-8 string constant for the namespace element will be: “spBv1.0” + */ + private String namespace; + + /** + * The ID of the logical grouping of Edge of Network (EoN) Nodes and devices. + */ + private String groupId; + + /** + * The ID of the Edge of Network (EoN) Node. + */ + private String edgeNodeId; + + /** + * The ID of the device. + */ + private String deviceId; + + /** + * The message type. + */ + private SparkplugMessageType type; + + /** + * Constructor (device). + * + * @param namespace the namespace. + * @param groupId the group ID. + * @param edgeNodeId the edge node ID. + * @param deviceId the device ID. + * @param type the message type. + */ + public SparkplugTopic(String namespace, String groupId, String edgeNodeId, String deviceId, SparkplugMessageType type) { + super(); + this.namespace = namespace; + this.groupId = groupId; + this.edgeNodeId = edgeNodeId; + this.deviceId = deviceId; + this.type = type; + } + + /** + * Constructor (node). + * + * @param namespace the namespace. + * @param groupId the group ID. + * @param edgeNodeId the edge node ID. + * @param type the message type. + */ + public SparkplugTopic(String namespace, String groupId, String edgeNodeId, SparkplugMessageType type) { + super(); + this.namespace = namespace; + this.groupId = groupId; + this.edgeNodeId = edgeNodeId; + this.deviceId = null; + this.type = type; + } + + public SparkplugTopic(SparkplugTopic sparkplugTopic, SparkplugMessageType type) { + super(); + this.namespace = sparkplugTopic.namespace; + this.groupId = sparkplugTopic.groupId; + this.edgeNodeId = sparkplugTopic.edgeNodeId; + this.deviceId = null; + this.type = type; + } + public SparkplugTopic(SparkplugTopic sparkplugTopic, SparkplugMessageType type, String deviceId) { + super(); + this.namespace = sparkplugTopic.namespace; + this.groupId = sparkplugTopic.groupId; + this.edgeNodeId = sparkplugTopic.edgeNodeId; + this.deviceId = deviceId; + this.type = type; + } + + /** + * @return the Sparkplug namespace version + */ + public String getNamespace() { + return namespace; + } + + /** + * Returns the ID of the logical grouping of Edge of Network (EoN) Nodes and devices. + * + * @return the group ID + */ + public String getGroupId() { + return groupId; + } + + /** + * @return the ID of the Edge of Network (EoN) Node + */ + public String getEdgeNodeId() { + return edgeNodeId; + } + + /** + * @return the device ID + */ + public String getDeviceId() { + return deviceId; + } + + /** + * @return the message type + */ + public SparkplugMessageType getType() { + return type; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getNamespace()).append("/") + .append(getGroupId()).append("/") + .append(getType()).append("/") + .append(getEdgeNodeId()); + if (getDeviceId() != null) { + sb.append("/").append(getDeviceId()); + } + return sb.toString(); + } + + /** + * @param type the type to check + * @return true if this topic's type matches the passes in type, false otherwise + */ + public boolean isType(SparkplugMessageType type) { + return this.type != null && this.type.equals(type); + } + + public boolean isNode() { + return this.deviceId == null; + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java new file mode 100644 index 0000000000..58b4319781 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java @@ -0,0 +1,116 @@ +/** + * Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; + +import java.util.HashMap; +import java.util.Map; + +/** + * Provides utility methods for handling Sparkplug MQTT message topics. + */ +public class SparkplugTopicUtil { + + private static final Map SPLIT_TOPIC_CACHE = new HashMap(); + private static final String TOPIC_INVALID_NUMBER = "Invalid number of topic elements: "; + public static final String NAMESPACE = "spBv1.0"; + + public static String[] getSplitTopic(String topic) { + String[] splitTopic = SPLIT_TOPIC_CACHE.get(topic); + if (splitTopic == null) { + splitTopic = topic.split("/"); + SPLIT_TOPIC_CACHE.put(topic, splitTopic); + } + + return splitTopic; + } + + /** + * Serializes a {@link SparkplugTopic} instance in to a JSON string. + * + * @param topic a {@link SparkplugTopic} instance + * @return a JSON string + * @throws JsonProcessingException + */ + public static String sparkplugTopicToString(SparkplugTopic topic) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(topic); + } + + /** + * Parses a Sparkplug MQTT message topic string and returns a {@link SparkplugTopic} instance. + * + * @param topic a topic string + * @return a {@link SparkplugTopic} instance + * @throws ThingsboardException if an error occurs while parsing + */ + public static SparkplugTopic parseTopicSubscribe(String topic) throws ThingsboardException { + // TODO "+", "$" + topic = topic.indexOf("#") > 0 ? topic.substring(0, topic.indexOf("#")) : topic; + return parseTopic(SparkplugTopicUtil.getSplitTopic(topic)); + } + + public static SparkplugTopic parseTopicPublish(String topic) throws ThingsboardException { + if (topic.contains("#") || topic.contains("$") || topic.contains("+")) { + throw new ThingsboardException("Invalid of topic elements for Publish", ThingsboardErrorCode.INVALID_ARGUMENTS); + } else { + String[] splitTopic = SparkplugTopicUtil.getSplitTopic(topic); + if (splitTopic.length < 4 || splitTopic.length > 5) { + throw new ThingsboardException(TOPIC_INVALID_NUMBER + splitTopic.length, ThingsboardErrorCode.INVALID_ARGUMENTS); + } + return parseTopic(splitTopic); + } + } + + /** + * Parses a Sparkplug MQTT message topic string and returns a {@link SparkplugTopic} instance. + * + * @param splitTopic a topic split into tokens + * @return a {@link SparkplugTopic} instance + * @throws Exception if an error occurs while parsing + */ + @SuppressWarnings("incomplete-switch") + public static SparkplugTopic parseTopic(String[] splitTopic) throws ThingsboardException { + int length = splitTopic.length; + if (length == 0) { + throw new ThingsboardException(TOPIC_INVALID_NUMBER + length, ThingsboardErrorCode.INVALID_ARGUMENTS); + } else { + SparkplugMessageType type; + String namespace, edgeNodeId, groupId, deviceId; + namespace = validateNameSpace(splitTopic[0]); + groupId = length > 1 ? splitTopic[1] : null; + type = length > 2 ? SparkplugMessageType.parseMessageType(splitTopic[2]) : null; + edgeNodeId = length > 3 ? splitTopic[3] : null; + deviceId = length > 4 ? splitTopic[4] : null; + return new SparkplugTopic(namespace, groupId, edgeNodeId, deviceId, type); + } + } + + /** + * For the Sparkplug™ B version of the specification, the UTF-8 string constant for the namespace element will be: "spBv1.0" + * @param nameSpace + * @return + */ + private static String validateNameSpace(String nameSpace) throws ThingsboardException { + if (NAMESPACE.equals(nameSpace)) return nameSpace; + throw new ThingsboardException("The namespace [" + nameSpace + "] is not valid and must be [" + NAMESPACE + "] for the Sparkplug™ B version.", ThingsboardErrorCode.INVALID_ARGUMENTS); + } + +} diff --git a/common/transport/mqtt/src/main/proto/sparkplug.proto b/common/transport/mqtt/src/main/proto/sparkplug.proto new file mode 100644 index 0000000000..0d757391ff --- /dev/null +++ b/common/transport/mqtt/src/main/proto/sparkplug.proto @@ -0,0 +1,204 @@ +/** + * Copyright © 2016-2023 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. + */ +syntax = "proto3"; + +import "google/protobuf/any.proto"; + + +option java_package = "org.thingsboard.server.gen.transport.mqtt"; +option java_outer_classname = "SparkplugBProto"; + +message Payload { + /* + // Indexes of Data Types + // Unknown placeholder for future expansion. + Unknown = 0; + // Basic Types + Int8 = 1; + Int16 = 2; + Int32 = 3; + Int64 = 4; + UInt8 = 5; + UInt16 = 6; + UInt32 = 7; + UInt64 = 8; + Float = 9; + Double = 10; + Boolean = 11; + String = 12; + DateTime = 13; + Text = 14; + // Additional Metric Types + UUID = 15; + DataSet = 16; + Bytes = 17; + File = 18; + Template = 19; + + // Additional PropertyValue Types + PropertySet = 20; + PropertySetList = 21; + */ + + message Template { + + message Parameter { + optional string name = 1; + optional uint32 type = 2; + + oneof value { + uint32 int_value = 3; + uint64 long_value = 4; + float float_value = 5; + double double_value = 6; + bool boolean_value = 7; + string string_value = 8; + ParameterValueExtension extension_value = 9; + } + + message ParameterValueExtension { + google.protobuf.Any extensions = 1; + } + } + + optional string version = 1; // The version of the Template to prevent mismatches + repeated Metric metrics = 2; // Each metric is the name of the metric and the datatype of the member but does not contain a value + repeated Parameter parameters = 3; + optional string template_ref = 4; // Reference to a template if this is extending a Template or an instance - must exist if an instance + optional bool is_definition = 5; + google.protobuf.Any extensions = 6; + } + + message DataSet { + + message DataSetValue { + + oneof value { + uint32 int_value = 1; + uint64 long_value = 2; + float float_value = 3; + double double_value = 4; + bool boolean_value = 5; + string string_value = 6; + DataSetValueExtension extension_value = 7; + } + + message DataSetValueExtension { + google.protobuf.Any extensions = 1; + } + } + + message Row { + repeated DataSetValue elements = 1; + google.protobuf.Any extensions = 2; // For third party extensions + } + + optional uint64 num_of_columns = 1; + repeated string columns = 2; + repeated uint32 types = 3; + repeated Row rows = 4; + google.protobuf.Any extensions = 5; // For third party extensions + } + + message PropertyValue { + + optional uint32 type = 1; + optional bool is_null = 2; + + oneof value { + uint32 int_value = 3; + uint64 long_value = 4; + float float_value = 5; + double double_value = 6; + bool boolean_value = 7; + string string_value = 8; + PropertySet propertyset_value = 9; + PropertySetList propertysets_value = 10; // List of Property Values + PropertyValueExtension extension_value = 11; + } + + message PropertyValueExtension { + google.protobuf.Any extensions = 1; + } + } + + message PropertySet { + repeated string keys = 1; // Names of the properties + repeated PropertyValue values = 2; + google.protobuf.Any extensions = 3; + } + + message PropertySetList { + repeated PropertySet propertyset = 1; + google.protobuf.Any extensions = 2; + } + + message MetaData { + // Bytes specific metadata + optional bool is_multi_part = 1; + + // General metadata + optional string content_type = 2; // Content/Media type + optional uint64 size = 3; // File size, String size, Multi-part size, etc + optional uint64 seq = 4; // Sequence number for multi-part messages + + // File metadata + optional string file_name = 5; // File name + optional string file_type = 6; // File type (i.e. xml, json, txt, cpp, etc) + optional string md5 = 7; // md5 of data + + // Catchalls and future expansion + optional string description = 8; // Could be anything such as json or xml of custom properties + google.protobuf.Any extensions = 9; + } + + message Metric { + + optional string name = 1; // Metric name - should only be included on birth + optional uint64 alias = 2; // Metric alias - tied to name on birth and included in all later DATA messages + optional uint64 timestamp = 3; // Timestamp associated with data acquisition time + optional uint32 datatype = 4; // DataType of the metric/tag value + optional bool is_historical = 5; // If this is historical data and should not update real time tag + optional bool is_transient = 6; // Tells consuming clients such as MQTT Engine to not store this as a tag + optional bool is_null = 7; // If this is null - explicitly say so rather than using -1, false, etc for some datatypes. + optional MetaData metadata = 8; // Metadata for the payload + optional PropertySet properties = 9; + + oneof value { + uint32 int_value = 10; + uint64 long_value = 11; + float float_value = 12; + double double_value = 13; + bool boolean_value = 14; + string string_value = 15; + bytes bytes_value = 16; // Bytes, File + DataSet dataset_value = 17; + Template template_value = 18; + MetricValueExtension extension_value = 19; + } + + message MetricValueExtension { + google.protobuf.Any extensions = 1; + } + } + + optional uint64 timestamp = 1; // Timestamp at message sending time + repeated Metric metrics = 2; // Repeated forever - no limit in Google Protobufs + optional uint64 seq = 3; // Sequence number + optional string uuid = 4; // UUID to track message type in terms of schema definitions + optional bytes body = 5; // To optionally bypass the whole definition above + google.protobuf.Any extensions = 6; +} \ No newline at end of file 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 04bf2b964d..dc2ad73cc1 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 @@ -301,8 +301,8 @@ public class DefaultTransportService implements TransportService { @Override public TransportProtos.GetEntityProfileResponseMsg getEntityProfile(TransportProtos.GetEntityProfileRequestMsg msg) { - TbProtoQueueMsg protoMsg = - new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setEntityProfileRequestMsg(msg).build()); + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setEntityProfileRequestMsg(msg).build()); try { TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); return response.getValue().getEntityProfileResponseMsg(); @@ -313,8 +313,8 @@ public class DefaultTransportService implements TransportService { @Override public List getQueueRoutingInfo(TransportProtos.GetAllQueueRoutingInfoRequestMsg msg) { - TbProtoQueueMsg protoMsg = - new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setGetAllQueueRoutingInfoRequestMsg(msg).build()); + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setGetAllQueueRoutingInfoRequestMsg(msg).build()); try { TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); return response.getValue().getGetQueueRoutingInfoResponseMsgsList(); @@ -325,8 +325,8 @@ public class DefaultTransportService implements TransportService { @Override public TransportProtos.GetResourceResponseMsg getResource(TransportProtos.GetResourceRequestMsg msg) { - TbProtoQueueMsg protoMsg = - new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setResourceRequestMsg(msg).build()); + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setResourceRequestMsg(msg).build()); try { TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); return response.getValue().getResourceResponseMsg(); @@ -337,8 +337,8 @@ public class DefaultTransportService implements TransportService { @Override public TransportProtos.GetSnmpDevicesResponseMsg getSnmpDevicesIds(TransportProtos.GetSnmpDevicesRequestMsg requestMsg) { - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>( - UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder() + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>( + UUID.randomUUID(), TransportApiRequestMsg.newBuilder() .setSnmpDevicesRequestMsg(requestMsg) .build() ); @@ -354,7 +354,7 @@ public class DefaultTransportService implements TransportService { @Override public TransportProtos.GetDeviceResponseMsg getDevice(TransportProtos.GetDeviceRequestMsg requestMsg) { TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>( - UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder() + UUID.randomUUID(), TransportApiRequestMsg.newBuilder() .setDeviceRequestMsg(requestMsg) .build() ); @@ -374,7 +374,7 @@ public class DefaultTransportService implements TransportService { @Override public TransportProtos.GetDeviceCredentialsResponseMsg getDeviceCredentials(TransportProtos.GetDeviceCredentialsRequestMsg requestMsg) { TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>( - UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder() + UUID.randomUUID(), TransportApiRequestMsg.newBuilder() .setDeviceCredentialsRequestMsg(requestMsg) .build() ); @@ -720,8 +720,8 @@ public class DefaultTransportService implements TransportService { @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.GetOtaPackageRequestMsg msg, TransportServiceCallback callback) { if (checkLimits(sessionInfo, msg, callback)) { - TbProtoQueueMsg protoMsg = - new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setOtaPackageRequestMsg(msg).build()); + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setOtaPackageRequestMsg(msg).build()); AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), response -> { callback.onSuccess(response.getValue().getOtaPackageResponseMsg()); @@ -864,7 +864,7 @@ public class DefaultTransportService implements TransportService { } } - protected void processToTransportMsg(TransportProtos.ToTransportMsg toSessionMsg) { + protected void processToTransportMsg(ToTransportMsg toSessionMsg) { UUID sessionId = new UUID(toSessionMsg.getSessionIdMSB(), toSessionMsg.getSessionIdLSB()); SessionMetaData md = sessions.get(sessionId); if (md != null) { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java index e226067042..4870cfc3ca 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java @@ -20,6 +20,8 @@ import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.gen.transport.TransportProtos; @@ -44,7 +46,7 @@ public abstract class DeviceAwareSessionContext implements SessionContext { protected volatile DeviceProfile deviceProfile; @Getter @Setter - private volatile TransportProtos.SessionInfoProto sessionInfo; + protected volatile TransportProtos.SessionInfoProto sessionInfo; @Setter private volatile boolean connected; @@ -81,4 +83,14 @@ public abstract class DeviceAwareSessionContext implements SessionContext { public void setDisconnected() { this.connected = false; } + + public boolean isSparkplug() { + DeviceProfileTransportConfiguration transportConfiguration = this.deviceProfile.getProfileData().getTransportConfiguration(); + if (transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) { + return ((MqttDeviceProfileTransportConfiguration) transportConfiguration).isSparkplug(); + } else { + return false; + } + } + } diff --git a/common/transport/transport-api/src/test/java/JsonConverterTest.java b/common/transport/transport-api/src/test/java/JsonConverterTest.java index a60516e784..39ed04a29a 100644 --- a/common/transport/transport-api/src/test/java/JsonConverterTest.java +++ b/common/transport/transport-api/src/test/java/JsonConverterTest.java @@ -16,20 +16,18 @@ import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import java.util.ArrayList; -@RunWith(MockitoJUnitRunner.class) public class JsonConverterTest { - private static final JsonParser JSON_PARSER = new JsonParser(); + private final JsonParser JSON_PARSER = new JsonParser(); - @Before + @BeforeEach public void before() { JsonConverter.setTypeCastEnabled(true); } @@ -88,15 +86,19 @@ public class JsonConverterTest { Assert.assertEquals("10000000000000000000", result.get(0L).get(0).getStrValue().get()); } - @Test(expected = JsonSyntaxException.class) + @Test public void testParseBigDecimalOutOfLongRangeWithoutParsing() { JsonConverter.setTypeCastEnabled(false); - JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 89701010051400054084}"), 0L); + Assertions.assertThrows(JsonSyntaxException.class, () -> { + JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 89701010051400054084}"), 0L); + }); } - @Test(expected = JsonSyntaxException.class) + @Test public void testParseBigDecimalOutOfLongRangeWithoutParsing2() { JsonConverter.setTypeCastEnabled(false); - JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); + Assertions.assertThrows(JsonSyntaxException.class, () -> { + JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); + }); } } diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 9120b45b6b..4638b81958 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -155,6 +155,14 @@ public class JacksonUtil { return mapper.createObjectNode(); } + public static ArrayNode newArrayNode() { + return newArrayNode(OBJECT_MAPPER); + } + + public static ArrayNode newArrayNode(ObjectMapper mapper) { + return mapper.createArrayNode(); + } + public static T clone(T value) { @SuppressWarnings("unchecked") Class valueClass = (Class) value.getClass(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java index 1905a0b784..c839b8ed69 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.dao.exception.DataValidationException; @@ -54,17 +55,21 @@ public abstract class BaseAdminSettingsServiceTest extends AbstractServiceTest { Assert.assertEquals(adminSettings.getJsonValue(), savedAdminSettings.getJsonValue()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveAdminSettingsWithEmptyKey() { AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(SYSTEM_TENANT_ID, "mail"); adminSettings.setKey(null); - adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings); + Assertions.assertThrows(DataValidationException.class, () -> { + adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings); + }); } - @Test(expected = DataValidationException.class) + @Test public void testChangeAdminSettingsKey() { AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(SYSTEM_TENANT_ID, "mail"); adminSettings.setKey("newKey"); - adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings); + Assertions.assertThrows(DataValidationException.class, () -> { + adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings); + }); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetProfileServiceTest.java index ea0d9abedf..091b309269 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetProfileServiceTest.java @@ -23,6 +23,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; @@ -150,22 +151,26 @@ public abstract class BaseAssetProfileServiceTest extends AbstractServiceTest { Assert.assertEquals(savedAssetProfile2.getId(), defaultAssetProfile.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveAssetProfileWithEmptyName() { AssetProfile assetProfile = new AssetProfile(); assetProfile.setTenantId(tenantId); - assetProfileService.saveAssetProfile(assetProfile); + Assertions.assertThrows(DataValidationException.class, () -> { + assetProfileService.saveAssetProfile(assetProfile); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveAssetProfileWithSameName() { AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile"); assetProfileService.saveAssetProfile(assetProfile); AssetProfile assetProfile2 = this.createAssetProfile(tenantId, "Asset Profile"); - assetProfileService.saveAssetProfile(assetProfile2); + Assertions.assertThrows(DataValidationException.class, () -> { + assetProfileService.saveAssetProfile(assetProfile2); + }); } - @Test(expected = DataValidationException.class) + @Test public void testDeleteAssetProfileWithExistingAsset() { AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile"); AssetProfile savedAssetProfile = assetProfileService.saveAssetProfile(assetProfile); @@ -174,7 +179,9 @@ public abstract class BaseAssetProfileServiceTest extends AbstractServiceTest { asset.setName("Test asset"); asset.setAssetProfileId(savedAssetProfile.getId()); assetService.saveAsset(asset); - assetProfileService.deleteAssetProfile(tenantId, savedAssetProfile.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + assetProfileService.deleteAssetProfile(tenantId, savedAssetProfile.getId()); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java index 9fe9752a85..108fcb70d9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; @@ -83,63 +84,73 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, savedAsset.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveAssetWithEmptyName() { Asset asset = new Asset(); asset.setTenantId(tenantId); asset.setType("default"); - assetService.saveAsset(asset); + Assertions.assertThrows(DataValidationException.class, () -> { + assetService.saveAsset(asset); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveAssetWithEmptyTenant() { Asset asset = new Asset(); asset.setName("My asset"); asset.setType("default"); - assetService.saveAsset(asset); + Assertions.assertThrows(DataValidationException.class, () -> { + assetService.saveAsset(asset); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveAssetWithInvalidTenant() { Asset asset = new Asset(); asset.setName("My asset"); asset.setType("default"); asset.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - assetService.saveAsset(asset); + Assertions.assertThrows(DataValidationException.class, () -> { + assetService.saveAsset(asset); + }); } - @Test(expected = DataValidationException.class) + @Test public void testAssignAssetToNonExistentCustomer() { Asset asset = new Asset(); asset.setName("My asset"); asset.setType("default"); asset.setTenantId(tenantId); - asset = assetService.saveAsset(asset); + Asset savedAsset = assetService.saveAsset(asset); try { - assetService.assignAssetToCustomer(tenantId, asset.getId(), new CustomerId(Uuids.timeBased())); + Assertions.assertThrows(DataValidationException.class, () -> { + assetService.assignAssetToCustomer(tenantId, savedAsset.getId(), new CustomerId(Uuids.timeBased())); + }); } finally { - assetService.deleteAsset(tenantId, asset.getId()); + assetService.deleteAsset(tenantId, savedAsset.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testAssignAssetToCustomerFromDifferentTenant() { Asset asset = new Asset(); asset.setName("My asset"); asset.setType("default"); asset.setTenantId(tenantId); - asset = assetService.saveAsset(asset); + Asset savedAsset = assetService.saveAsset(asset); Tenant tenant = new Tenant(); tenant.setTitle("Test different tenant"); tenant = tenantService.saveTenant(tenant); Customer customer = new Customer(); customer.setTenantId(tenant.getId()); customer.setTitle("Test different customer"); - customer = customerService.saveCustomer(customer); + Customer savedCustomer = customerService.saveCustomer(customer); try { - assetService.assignAssetToCustomer(tenantId, asset.getId(), customer.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + assetService.assignAssetToCustomer(tenantId, savedAsset.getId(), savedCustomer.getId()); + }); } finally { - assetService.deleteAsset(tenantId, asset.getId()); + assetService.deleteAsset(tenantId, savedAsset.getId()); tenantService.deleteTenant(tenant.getId()); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java index 11d82ad963..18bf036495 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java @@ -24,6 +24,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.StringUtils; @@ -99,35 +100,43 @@ public abstract class BaseCustomerServiceTest extends AbstractServiceTest { customerService.deleteCustomer(tenantId, savedCustomer.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveCustomerWithEmptyTitle() { Customer customer = new Customer(); customer.setTenantId(tenantId); - customerService.saveCustomer(customer); + Assertions.assertThrows(DataValidationException.class, () -> { + customerService.saveCustomer(customer); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveCustomerWithEmptyTenant() { Customer customer = new Customer(); customer.setTitle("My customer"); - customerService.saveCustomer(customer); + Assertions.assertThrows(DataValidationException.class, () -> { + customerService.saveCustomer(customer); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveCustomerWithInvalidTenant() { Customer customer = new Customer(); customer.setTitle("My customer"); customer.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - customerService.saveCustomer(customer); + Assertions.assertThrows(DataValidationException.class, () -> { + customerService.saveCustomer(customer); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveCustomerWithInvalidEmail() { Customer customer = new Customer(); customer.setTenantId(tenantId); customer.setTitle("My customer"); customer.setEmail("invalid@mail"); - customerService.saveCustomer(customer); + Assertions.assertThrows(DataValidationException.class, () -> { + customerService.saveCustomer(customer); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java index 88d66e9586..4dc28a210a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; @@ -82,58 +83,68 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { dashboardService.deleteDashboard(tenantId, savedDashboard.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDashboardWithEmptyTitle() { Dashboard dashboard = new Dashboard(); dashboard.setTenantId(tenantId); - dashboardService.saveDashboard(dashboard); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.saveDashboard(dashboard); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDashboardWithEmptyTenant() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); - dashboardService.saveDashboard(dashboard); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.saveDashboard(dashboard); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDashboardWithInvalidTenant() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); dashboard.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - dashboardService.saveDashboard(dashboard); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.saveDashboard(dashboard); + }); } - @Test(expected = DataValidationException.class) + @Test public void testAssignDashboardToNonExistentCustomer() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); dashboard.setTenantId(tenantId); - dashboard = dashboardService.saveDashboard(dashboard); + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); try { - dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), new CustomerId(Uuids.timeBased())); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.assignDashboardToCustomer(tenantId, savedDashboard.getId(), new CustomerId(Uuids.timeBased())); + }); } finally { - dashboardService.deleteDashboard(tenantId, dashboard.getId()); + dashboardService.deleteDashboard(tenantId, savedDashboard.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testAssignDashboardToCustomerFromDifferentTenant() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); dashboard.setTenantId(tenantId); - dashboard = dashboardService.saveDashboard(dashboard); + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); Tenant tenant = new Tenant(); tenant.setTitle("Test different tenant [dashboard]"); tenant = tenantService.saveTenant(tenant); Customer customer = new Customer(); customer.setTenantId(tenant.getId()); customer.setTitle("Test different customer"); - customer = customerService.saveCustomer(customer); + Customer savedCustomer = customerService.saveCustomer(customer); try { - dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), customer.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.assignDashboardToCustomer(tenantId, savedDashboard.getId(), savedCustomer.getId()); + }); } finally { - dashboardService.deleteDashboard(tenantId, dashboard.getId()); + dashboardService.deleteDashboard(tenantId, savedDashboard.getId()); tenantService.deleteTenant(tenant.getId()); } } @@ -389,25 +400,27 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { tenantService.deleteTenant(tenantId); } - @Test(expected = DataValidationException.class) + @Test public void testAssignDashboardToNonExistentEdge() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); dashboard.setTenantId(tenantId); - dashboard = dashboardService.saveDashboard(dashboard); + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); try { - dashboardService.assignDashboardToEdge(tenantId, dashboard.getId(), new EdgeId(Uuids.timeBased())); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.assignDashboardToEdge(tenantId, savedDashboard.getId(), new EdgeId(Uuids.timeBased())); + }); } finally { - dashboardService.deleteDashboard(tenantId, dashboard.getId()); + dashboardService.deleteDashboard(tenantId, savedDashboard.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testAssignDashboardToEdgeFromDifferentTenant() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); dashboard.setTenantId(tenantId); - dashboard = dashboardService.saveDashboard(dashboard); + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); Tenant tenant = new Tenant(); tenant.setTitle("Test different tenant [edge]"); tenant = tenantService.saveTenant(tenant); @@ -418,11 +431,13 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { edge.setType("default"); edge.setSecret(StringUtils.randomAlphanumeric(20)); edge.setRoutingKey(StringUtils.randomAlphanumeric(20)); - edge = edgeService.saveEdge(edge); + Edge savedEdge = edgeService.saveEdge(edge); try { - dashboardService.assignDashboardToEdge(tenantId, dashboard.getId(), edge.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.assignDashboardToEdge(tenantId, savedDashboard.getId(), savedEdge.getId()); + }); } finally { - dashboardService.deleteDashboard(tenantId, dashboard.getId()); + dashboardService.deleteDashboard(tenantId, savedDashboard.getId()); tenantService.deleteTenant(tenant.getId()); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java index a1d4543c58..f3e052b3e9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.DeviceCredentialsId; @@ -47,13 +48,15 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe tenantService.deleteTenant(tenantId); } - @Test(expected = DataValidationException.class) + @Test public void testCreateDeviceCredentials() { DeviceCredentials deviceCredentials = new DeviceCredentials(); - deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceCredentialsWithEmptyDevice() { Device device = new Device(); device.setName("My device"); @@ -63,13 +66,15 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); deviceCredentials.setDeviceId(null); try { - deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + }); } finally { deviceService.deleteDevice(tenantId, device.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceCredentialsWithEmptyCredentialsType() { Device device = new Device(); device.setName("My device"); @@ -79,13 +84,15 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); deviceCredentials.setCredentialsType(null); try { - deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + }); } finally { deviceService.deleteDevice(tenantId, device.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceCredentialsWithEmptyCredentialsId() { Device device = new Device(); device.setName("My device"); @@ -95,13 +102,15 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); deviceCredentials.setCredentialsId(null); try { - deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + }); } finally { deviceService.deleteDevice(tenantId, device.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveNonExistentDeviceCredentials() { Device device = new Device(); device.setName("My device"); @@ -115,13 +124,15 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe newDeviceCredentials.setCredentialsType(deviceCredentials.getCredentialsType()); newDeviceCredentials.setCredentialsId(deviceCredentials.getCredentialsId()); try { - deviceCredentialsService.updateDeviceCredentials(tenantId, newDeviceCredentials); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceCredentialsService.updateDeviceCredentials(tenantId, newDeviceCredentials); + }); } finally { deviceService.deleteDevice(tenantId, device.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceCredentialsWithNonExistentDevice() { Device device = new Device(); device.setName("My device"); @@ -131,7 +142,9 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); deviceCredentials.setDeviceId(new DeviceId(Uuids.timeBased())); try { - deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + }); } finally { deviceService.deleteDevice(tenantId, device.getId()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java index d669c24cd6..77a8a479f2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java @@ -24,6 +24,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -196,23 +197,27 @@ public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest { Assert.assertEquals(savedDeviceProfile2.getId(), defaultDeviceProfile.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceProfileWithEmptyName() { DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setTenantId(tenantId); - deviceProfileService.saveDeviceProfile(deviceProfile); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceProfileService.saveDeviceProfile(deviceProfile); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceProfileWithSameName() { DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); deviceProfileService.saveDeviceProfile(deviceProfile); DeviceProfile deviceProfile2 = this.createDeviceProfile(tenantId, "Device Profile"); - deviceProfileService.saveDeviceProfile(deviceProfile2); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceProfileService.saveDeviceProfile(deviceProfile2); + }); } @Ignore - @Test(expected = DataValidationException.class) + @Test public void testChangeDeviceProfileTypeWithExistingDevices() { DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); @@ -224,10 +229,12 @@ public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest { deviceService.saveDevice(device); //TODO: once we have more profile types, we should test that we can not change profile type in runtime and uncomment the @Ignore. // savedDeviceProfile.setType(DeviceProfileType.LWM2M); - deviceProfileService.saveDeviceProfile(savedDeviceProfile); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceProfileService.saveDeviceProfile(savedDeviceProfile); + }); } - @Test(expected = DataValidationException.class) + @Test public void testChangeDeviceProfileTransportTypeWithExistingDevices() { DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); @@ -238,10 +245,12 @@ public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest { device.setDeviceProfileId(savedDeviceProfile.getId()); deviceService.saveDevice(device); savedDeviceProfile.setTransportType(DeviceTransportType.MQTT); - deviceProfileService.saveDeviceProfile(savedDeviceProfile); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceProfileService.saveDeviceProfile(savedDeviceProfile); + }); } - @Test(expected = DataValidationException.class) + @Test public void testDeleteDeviceProfileWithExistingDevice() { DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); @@ -251,7 +260,9 @@ public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest { device.setType("default"); device.setDeviceProfileId(savedDeviceProfile.getId()); deviceService.saveDevice(device); - deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId()); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index ea2fa9546f..010f56235f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java @@ -21,6 +21,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.junit.rules.ExpectedException; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; @@ -91,7 +92,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deleteDevice(tenantId, device); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDevicesWithMaxDeviceOutOfLimit() { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(1).build()); @@ -102,7 +103,9 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { this.saveDevice(tenantId, "My first device"); Assert.assertEquals(1, deviceService.countByTenantId(tenantId)); - this.saveDevice(tenantId, "My second device that out of maxDeviceCount limit"); + Assertions.assertThrows(DataValidationException.class, () -> { + this.saveDevice(tenantId, "My second device that out of maxDeviceCount limit"); + }); } @Test @@ -246,63 +249,73 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.saveDevice(savedDevice); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceWithEmptyName() { Device device = new Device(); device.setType("default"); device.setTenantId(tenantId); - deviceService.saveDevice(device); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceService.saveDevice(device); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceWithEmptyTenant() { Device device = new Device(); device.setName("My device"); device.setType("default"); - deviceService.saveDevice(device); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceService.saveDevice(device); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceWithInvalidTenant() { Device device = new Device(); device.setName("My device"); device.setType("default"); device.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - deviceService.saveDevice(device); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceService.saveDevice(device); + }); } - @Test(expected = DataValidationException.class) + @Test public void testAssignDeviceToNonExistentCustomer() { Device device = new Device(); device.setName("My device"); device.setType("default"); device.setTenantId(tenantId); - device = deviceService.saveDevice(device); + Device savedDevice = deviceService.saveDevice(device); try { - deviceService.assignDeviceToCustomer(tenantId, device.getId(), new CustomerId(Uuids.timeBased())); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceService.assignDeviceToCustomer(tenantId, savedDevice.getId(), new CustomerId(Uuids.timeBased())); + }); } finally { - deviceService.deleteDevice(tenantId, device.getId()); + deviceService.deleteDevice(tenantId, savedDevice.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testAssignDeviceToCustomerFromDifferentTenant() { Device device = new Device(); device.setName("My device"); device.setType("default"); device.setTenantId(tenantId); - device = deviceService.saveDevice(device); + Device savedDevice = deviceService.saveDevice(device); Tenant tenant = new Tenant(); tenant.setTitle("Test different tenant"); tenant = tenantService.saveTenant(tenant); Customer customer = new Customer(); customer.setTenantId(tenant.getId()); customer.setTitle("Test different customer"); - customer = customerService.saveCustomer(customer); + Customer savedCustomer = customerService.saveCustomer(customer); try { - deviceService.assignDeviceToCustomer(tenantId, device.getId(), customer.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceService.assignDeviceToCustomer(tenantId, savedDevice.getId(), savedCustomer.getId()); + }); } finally { - deviceService.deleteDevice(tenantId, device.getId()); + deviceService.deleteDevice(tenantId, savedDevice.getId()); tenantService.deleteTenant(tenant.getId()); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java index 5aced56afd..70d41f4681 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java @@ -21,6 +21,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; @@ -86,57 +87,67 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { edgeService.deleteEdge(tenantId, savedEdge.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveEdgeWithEmptyName() { Edge edge = new Edge(); edge.setType("default"); edge.setTenantId(tenantId); - edgeService.saveEdge(edge); + Assertions.assertThrows(DataValidationException.class, () -> { + edgeService.saveEdge(edge); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveEdgeWithEmptyTenant() { Edge edge = new Edge(); edge.setName("My edge"); edge.setType("default"); - edgeService.saveEdge(edge); + Assertions.assertThrows(DataValidationException.class, () -> { + edgeService.saveEdge(edge); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveEdgeWithInvalidTenant() { Edge edge = new Edge(); edge.setName("My edge"); edge.setType("default"); edge.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - edgeService.saveEdge(edge); + Assertions.assertThrows(DataValidationException.class, () -> { + edgeService.saveEdge(edge); + }); } - @Test(expected = DataValidationException.class) + @Test public void testAssignEdgeToNonExistentCustomer() { Edge edge = constructEdge("My edge", "default"); - edge = edgeService.saveEdge(edge); + Edge savedEdge = edgeService.saveEdge(edge); try { - edgeService.assignEdgeToCustomer(tenantId, edge.getId(), new CustomerId(Uuids.timeBased())); + Assertions.assertThrows(DataValidationException.class, () -> { + edgeService.assignEdgeToCustomer(tenantId, savedEdge.getId(), new CustomerId(Uuids.timeBased())); + }); } finally { - edgeService.deleteEdge(tenantId, edge.getId()); + edgeService.deleteEdge(tenantId, savedEdge.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testAssignEdgeToCustomerFromDifferentTenant() { Edge edge = constructEdge("My edge", "default"); - edge = edgeService.saveEdge(edge); + Edge savedEdge = edgeService.saveEdge(edge); Tenant tenant = new Tenant(); tenant.setTitle("Test different tenant"); tenant = tenantService.saveTenant(tenant); Customer customer = new Customer(); customer.setTenantId(tenant.getId()); customer.setTitle("Test different customer"); - customer = customerService.saveCustomer(customer); + Customer savedCustomer = customerService.saveCustomer(customer); try { - edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customer.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + edgeService.assignEdgeToCustomer(tenantId, savedEdge.getId(), savedCustomer.getId()); + }); } finally { - edgeService.deleteEdge(tenantId, edge.getId()); + edgeService.deleteEdge(tenantId, savedEdge.getId()); tenantService.deleteTenant(tenant.getId()); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java index 3ab182519c..510ca0f2db 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java @@ -19,6 +19,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.MapperType; @@ -51,12 +52,14 @@ public abstract class BaseOAuth2ConfigTemplateServiceTest extends AbstractServic } - @Test(expected = DataValidationException.class) + @Test public void testSaveDuplicateProviderId() { OAuth2ClientRegistrationTemplate first = validClientRegistrationTemplate("providerId"); OAuth2ClientRegistrationTemplate second = validClientRegistrationTemplate("providerId"); oAuth2ConfigTemplateService.saveClientRegistrationTemplate(first); - oAuth2ConfigTemplateService.saveClientRegistrationTemplate(second); + Assertions.assertThrows(DataValidationException.class, () -> { + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(second); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java index 42ea18bdec..73dd08d53c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.oauth2.MapperType; @@ -61,7 +62,7 @@ public abstract class BaseOAuth2ServiceTest extends AbstractServiceTest { Assert.assertTrue(oAuth2Service.findOAuth2Info().getOauth2ParamsInfos().isEmpty()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveHttpAndMixedDomainsTogether() { OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( OAuth2ParamsInfo.builder() @@ -77,10 +78,12 @@ public abstract class BaseOAuth2ServiceTest extends AbstractServiceTest { )) .build() )); - oAuth2Service.saveOAuth2Info(oAuth2Info); + Assertions.assertThrows(DataValidationException.class, () -> { + oAuth2Service.saveOAuth2Info(oAuth2Info); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveHttpsAndMixedDomainsTogether() { OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( OAuth2ParamsInfo.builder() @@ -96,7 +99,9 @@ public abstract class BaseOAuth2ServiceTest extends AbstractServiceTest { )) .build() )); - oAuth2Service.saveOAuth2Info(oAuth2Info); + Assertions.assertThrows(DataValidationException.class, () -> { + oAuth2Service.saveOAuth2Info(oAuth2Info); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java index d4242fb1c4..af2f067c2e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java @@ -19,6 +19,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; @@ -139,7 +140,7 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queueService.deleteQueue(tenantId, foundQueue.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyName() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -149,10 +150,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithInvalidName() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -163,10 +166,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyTopic() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -176,10 +181,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithInvalidTopic() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -190,10 +197,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyPollInterval() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -203,10 +212,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyPartitions() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -216,10 +227,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyPackProcessingTimeout() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -229,10 +242,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPartitions(1); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptySubmitStrategy() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -242,10 +257,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPartitions(1); queue.setPackProcessingTimeout(2000); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyProcessingStrategy() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -255,10 +272,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPartitions(1); queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptySubmitStrategyType() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -270,10 +289,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.getSubmitStrategy().setType(null); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptySubmitStrategyBatchSize() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -286,10 +307,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.getSubmitStrategy().setType(SubmitStrategyType.BATCH); queue.getSubmitStrategy().setBatchSize(0); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyProcessingStrategyType() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -301,10 +324,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); queue.getProcessingStrategy().setType(null); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithNegativeProcessingStrategyRetries() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -316,10 +341,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); queue.getProcessingStrategy().setRetries(-1); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithNegativeProcessingStrategyFailurePercentage() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -331,10 +358,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); queue.getProcessingStrategy().setFailurePercentage(-1); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithNegativeProcessingStrategyPauseBetweenRetries() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -346,10 +375,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); queue.getProcessingStrategy().setPauseBetweenRetries(-1); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithProcessingStrategyPauseBetweenRetriesBiggerThenMaxPauseBetweenRetries() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -361,10 +392,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); queue.getProcessingStrategy().setPauseBetweenRetries(100); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithNotIsolatedTenant() { Tenant tenant = new Tenant(); tenant.setTitle("Not isolated tenant"); @@ -381,7 +414,9 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); try { - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } finally { tenantService.deleteTenant(savedTenant.getId()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java index 82c50f2438..c920b86bd5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java @@ -22,6 +22,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; @@ -334,28 +335,34 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { } } - @Test(expected = DataValidationException.class) + @Test public void testSaveRelationWithEmptyFrom() throws ExecutionException, InterruptedException { EntityRelation relation = new EntityRelation(); relation.setTo(new AssetId(Uuids.timeBased())); relation.setType(EntityRelation.CONTAINS_TYPE); - Assert.assertTrue(saveRelation(relation)); + Assertions.assertThrows(DataValidationException.class, () -> { + Assert.assertTrue(saveRelation(relation)); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveRelationWithEmptyTo() throws ExecutionException, InterruptedException { EntityRelation relation = new EntityRelation(); relation.setFrom(new AssetId(Uuids.timeBased())); relation.setType(EntityRelation.CONTAINS_TYPE); - Assert.assertTrue(saveRelation(relation)); + Assertions.assertThrows(DataValidationException.class, () -> { + Assert.assertTrue(saveRelation(relation)); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveRelationWithEmptyType() throws ExecutionException, InterruptedException { EntityRelation relation = new EntityRelation(); relation.setFrom(new AssetId(Uuids.timeBased())); relation.setTo(new AssetId(Uuids.timeBased())); - Assert.assertTrue(saveRelation(relation)); + Assertions.assertThrows(DataValidationException.class, () -> { + Assert.assertTrue(saveRelation(relation)); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java index 18c6e110d5..16901119a6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java @@ -21,6 +21,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.edge.Edge; @@ -86,19 +87,23 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainService.deleteRuleChainById(tenantId, savedRuleChain.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveRuleChainWithEmptyName() { RuleChain ruleChain = new RuleChain(); ruleChain.setTenantId(tenantId); - ruleChainService.saveRuleChain(ruleChain); + Assertions.assertThrows(DataValidationException.class, () -> { + ruleChainService.saveRuleChain(ruleChain); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveRuleChainWithInvalidTenant() { RuleChain ruleChain = new RuleChain(); ruleChain.setName("My RuleChain"); ruleChain.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - ruleChainService.saveRuleChain(ruleChain); + Assertions.assertThrows(DataValidationException.class, () -> { + ruleChainService.saveRuleChain(ruleChain); + }); } @Test @@ -321,14 +326,18 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainService.deleteRuleChainById(tenantId, savedRuleChainMetaData.getRuleChainId()); } - @Test(expected = DataValidationException.class) - public void testUpdateRuleChainMetaDataWithCirclingRelation() throws Exception { - ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation()); + @Test + public void testUpdateRuleChainMetaDataWithCirclingRelation() { + Assertions.assertThrows(DataValidationException.class, () -> { + ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation()); + }); } - @Test(expected = DataValidationException.class) - public void testUpdateRuleChainMetaDataWithCirclingRelation2() throws Exception { - ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation2()); + @Test + public void testUpdateRuleChainMetaDataWithCirclingRelation2() { + Assertions.assertThrows(DataValidationException.class, () -> { + ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation2()); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java index a21a8e5845..926f680e40 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.node.NullNode; import org.junit.After; import org.junit.Assert; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.Tenant; @@ -154,30 +155,36 @@ public abstract class BaseTenantProfileServiceTest extends AbstractServiceTest { Assert.assertEquals(savedTenantProfile2.getId(), defaultTenantProfile.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveTenantProfileWithEmptyName() { TenantProfile tenantProfile = new TenantProfile(); - tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + Assertions.assertThrows(DataValidationException.class, () -> { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveTenantProfileWithSameName() { TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); TenantProfile tenantProfile2 = this.createTenantProfile("Tenant Profile"); - tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile2); + Assertions.assertThrows(DataValidationException.class, () -> { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile2); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveSameTenantProfileWithDifferentIsolatedTbRuleEngine() { TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); savedTenantProfile.setIsolatedTbRuleEngine(true); addMainQueueConfig(savedTenantProfile); - tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile); + Assertions.assertThrows(DataValidationException.class, () -> { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile); + }); } - @Test(expected = DataValidationException.class) + @Test public void testDeleteTenantProfileWithExistingTenant() { TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); @@ -186,7 +193,9 @@ public abstract class BaseTenantProfileServiceTest extends AbstractServiceTest { tenant.setTenantProfileId(savedTenantProfile.getId()); tenant = tenantService.saveTenant(tenant); try { - tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + }); } finally { tenantService.deleteTenant(tenant.getId()); } @@ -294,7 +303,7 @@ public abstract class BaseTenantProfileServiceTest extends AbstractServiceTest { return tenantProfile; } - private void addMainQueueConfig(TenantProfile tenantProfile) { + public static void addMainQueueConfig(TenantProfile tenantProfile) { TenantProfileQueueConfiguration mainQueueConfiguration = new TenantProfileQueueConfiguration(); mainQueueConfiguration.setName(DataConstants.MAIN_QUEUE_NAME); mainQueueConfiguration.setTopic(DataConstants.MAIN_QUEUE_TOPIC); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java index 4e4fc44c56..3f8c33c5e7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service; import org.junit.Assert; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; @@ -52,8 +53,6 @@ import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TenantDao; @@ -121,18 +120,22 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { tenantService.deleteTenant(savedTenant.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveTenantWithEmptyTitle() { Tenant tenant = new Tenant(); - tenantService.saveTenant(tenant); + Assertions.assertThrows(DataValidationException.class, () -> { + tenantService.saveTenant(tenant); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveTenantWithInvalidEmail() { Tenant tenant = new Tenant(); tenant.setTitle("My tenant"); tenant.setEmail("invalid@mail"); - tenantService.saveTenant(tenant); + Assertions.assertThrows(DataValidationException.class, () -> { + tenantService.saveTenant(tenant); + }); } @Test @@ -301,23 +304,6 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { } - @Test(expected = DataValidationException.class) - public void testSaveTenantWithIsolatedProfileInMonolithSetup() { - TenantProfile tenantProfile = new TenantProfile(); - tenantProfile.setName("Isolated Tenant Profile"); - TenantProfileData profileData = new TenantProfileData(); - profileData.setConfiguration(new DefaultTenantProfileConfiguration()); - tenantProfile.setProfileData(profileData); - tenantProfile.setDefault(false); - tenantProfile.setIsolatedTbRuleEngine(true); - TenantProfile isolatedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); - - Tenant tenant = new Tenant(); - tenant.setTitle("Tenant"); - tenant.setTenantProfileId(isolatedTenantProfile.getId()); - tenantService.saveTenant(tenant); - } - @Test public void testGettingTenantAddingItToCache() { Tenant tenant = new Tenant(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java index d7885a1cb2..49ececc8e4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java @@ -19,6 +19,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.StringUtils; @@ -142,32 +143,40 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { userService.deleteUser(tenantId, savedUser.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveUserWithSameEmail() { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); tenantAdminUser.setEmail("sysadmin@thingsboard.org"); - userService.saveUser(tenantAdminUser); + Assertions.assertThrows(DataValidationException.class, () -> { + userService.saveUser(tenantAdminUser); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveUserWithInvalidEmail() { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); tenantAdminUser.setEmail("tenant_thingsboard.org"); - userService.saveUser(tenantAdminUser); + Assertions.assertThrows(DataValidationException.class, () -> { + userService.saveUser(tenantAdminUser); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveUserWithEmptyEmail() { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); tenantAdminUser.setEmail(null); - userService.saveUser(tenantAdminUser); + Assertions.assertThrows(DataValidationException.class, () -> { + userService.saveUser(tenantAdminUser); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveUserWithoutTenant() { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); tenantAdminUser.setTenantId(null); - userService.saveUser(tenantAdminUser); + Assertions.assertThrows(DataValidationException.class, () -> { + userService.saveUser(tenantAdminUser); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java index 404e6fe2e1..04cc1d8412 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java @@ -22,6 +22,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.widget.WidgetType; @@ -88,7 +89,7 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetTypeWithEmptyName() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); @@ -100,22 +101,26 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class)); try { - widgetTypeService.saveWidgetType(widgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(widgetType); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetTypeWithEmptyBundleAlias() throws IOException { WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); widgetType.setName("Widget Type"); widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class)); - widgetTypeService.saveWidgetType(widgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(widgetType); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetTypeWithEmptyDescriptor() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); @@ -128,13 +133,15 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setDescriptor(new ObjectMapper().readValue("{}", JsonNode.class)); try { - widgetTypeService.saveWidgetType(widgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(widgetType); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetTypeWithInvalidTenant() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); @@ -147,23 +154,27 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { widgetType.setName("Widget Type"); widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class)); try { - widgetTypeService.saveWidgetType(widgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(widgetType); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetTypeWithInvalidBundleAlias() throws IOException { WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); widgetType.setBundleAlias("some_alias"); widgetType.setName("Widget Type"); widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class)); - widgetTypeService.saveWidgetType(widgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(widgetType); + }); } - @Test(expected = DataValidationException.class) + @Test public void testUpdateWidgetTypeTenant() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); @@ -178,13 +189,15 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType); savedWidgetType.setTenantId(TenantId.fromUUID(ModelConstants.NULL_UUID)); try { - widgetTypeService.saveWidgetType(savedWidgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(savedWidgetType); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testUpdateWidgetTypeBundleAlias() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); @@ -199,13 +212,15 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType); savedWidgetType.setBundleAlias("some_alias"); try { - widgetTypeService.saveWidgetType(savedWidgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(savedWidgetType); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testUpdateWidgetTypeAlias() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); @@ -220,7 +235,9 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType); savedWidgetType.setAlias("some_alias"); try { - widgetTypeService.saveWidgetType(savedWidgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(savedWidgetType); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java index ac86572cdb..c28f5face7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -77,22 +78,26 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetsBundleWithEmptyTitle() { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); - widgetsBundleService.saveWidgetsBundle(widgetsBundle); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetsBundleService.saveWidgetsBundle(widgetsBundle); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetsBundleWithInvalidTenant() { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTitle("My widgets bundle"); widgetsBundle.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - widgetsBundleService.saveWidgetsBundle(widgetsBundle); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetsBundleService.saveWidgetsBundle(widgetsBundle); + }); } - @Test(expected = DataValidationException.class) + @Test public void testUpdateWidgetsBundleTenant() { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTitle("My widgets bundle"); @@ -100,13 +105,15 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); savedWidgetsBundle.setTenantId(TenantId.fromUUID(ModelConstants.NULL_UUID)); try { - widgetsBundleService.saveWidgetsBundle(savedWidgetsBundle); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetsBundleService.saveWidgetsBundle(savedWidgetsBundle); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testUpdateWidgetsBundleAlias() { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTitle("My widgets bundle"); @@ -114,7 +121,9 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); savedWidgetsBundle.setAlias("new_alias"); try { - widgetsBundleService.saveWidgetsBundle(savedWidgetsBundle); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetsBundleService.saveWidgetsBundle(savedWidgetsBundle); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java index 6e195b266c..40262ac2b8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java @@ -15,7 +15,8 @@ */ package org.thingsboard.server.dao.service; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.thingsboard.server.dao.exception.DataValidationException; public class DataValidatorTest { @@ -26,38 +27,50 @@ public class DataValidatorTest { DataValidator.validateEmail(email); } - @Test(expected = DataValidationException.class) + @Test public void validateInvalidEmail1() { String email = "test:1@mail.io"; - DataValidator.validateEmail(email); + Assertions.assertThrows(DataValidationException.class, () -> { + DataValidator.validateEmail(email); + }); } - @Test(expected = DataValidationException.class) + @Test public void validateInvalidEmail2() { String email = "test()1@mail.io"; - DataValidator.validateEmail(email); + Assertions.assertThrows(DataValidationException.class, () -> { + DataValidator.validateEmail(email); + }); } - @Test(expected = DataValidationException.class) + @Test public void validateInvalidEmail3() { String email = "test[]1@mail.io"; - DataValidator.validateEmail(email); + Assertions.assertThrows(DataValidationException.class, () -> { + DataValidator.validateEmail(email); + }); } - @Test(expected = DataValidationException.class) + @Test public void validateInvalidEmail4() { String email = "test\\1@mail.io"; - DataValidator.validateEmail(email); + Assertions.assertThrows(DataValidationException.class, () -> { + DataValidator.validateEmail(email); + }); } - @Test(expected = DataValidationException.class) + @Test public void validateInvalidEmail5() { String email = "test\"1@mail.io"; - DataValidator.validateEmail(email); + Assertions.assertThrows(DataValidationException.class, () -> { + DataValidator.validateEmail(email); + }); } - @Test(expected = DataValidationException.class) + @Test public void validateInvalidEmail6() { String email = "test<>1@mail.io"; - DataValidator.validateEmail(email); + Assertions.assertThrows(DataValidationException.class, () -> { + DataValidator.validateEmail(email); + }); } -} \ No newline at end of file +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java index fa1caca815..f2618da6b8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java @@ -16,7 +16,7 @@ package org.thingsboard.server.dao.service; import com.fasterxml.jackson.databind.node.TextNode; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.thingsboard.common.util.JacksonUtil; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java index 3d8652a7a0..bd761a084c 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -70,14 +71,18 @@ public class TbGetTelemetryNodeTest { } } - @Test(expected = IllegalArgumentException.class) + @Test public void givenAggregationWhiteSpace_whenParseAggregation_thenException() { - node.parseAggregationConfig(" "); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + node.parseAggregationConfig(" "); + }); } - @Test(expected = IllegalArgumentException.class) + @Test public void givenAggregationIncorrect_whenParseAggregation_thenException() { - node.parseAggregationConfig("TOP"); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + node.parseAggregationConfig("TOP"); + }); } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html index 18018742be..1ef7892ecb 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html @@ -15,7 +15,32 @@ limitations under the License. --> -
+ + + {{ 'device-profile.mqtt-device-topic-filters-spark-plug' | translate }} + +
+ + device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names + + + {{name}} + close + + + + + +
+
device-profile.mqtt-device-topic-filters
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts index a60f4ec5c4..cf931a7fed 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts @@ -41,6 +41,8 @@ import { import { isDefinedAndNotNull } from '@core/utils'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { MatChipInputEvent } from '@angular/material/chips'; @Component({ selector: 'tb-mqtt-device-profile-transport-configuration', @@ -77,6 +79,8 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control private propagateChange = (v: any) => { }; + separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + constructor(private store: Store, private fb: UntypedFormBuilder) { } @@ -92,6 +96,8 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control this.mqttDeviceProfileTransportConfigurationFormGroup = this.fb.group({ deviceAttributesTopic: [null, [Validators.required, this.validationMQTTTopic()]], deviceTelemetryTopic: [null, [Validators.required, this.validationMQTTTopic()]], + sparkplug: [false], + sparkplugAttributesMetricNames: [null], sendAckOnValidationException: [false, Validators.required], transportPayloadTypeConfiguration: this.fb.group({ transportPayloadType: [TransportPayloadType.JSON, Validators.required], @@ -102,7 +108,7 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control enableCompatibilityWithJsonPayloadFormat: [false, Validators.required], useJsonPayloadFormatForDefaultDownlinkTopics: [false, Validators.required] }) - }, {validator: this.uniqueDeviceTopicValidator} + }, {validators: this.uniqueDeviceTopicValidator} ); this.mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.transportPayloadType').valueChanges.pipe( takeUntil(this.destroy$) @@ -117,6 +123,17 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control .patchValue(false, {emitEvent: false}); } }); + this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + if (value) { + this.mqttDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); + this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').enable({emitEvent: false}); + this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').enable({emitEvent: false}); + } else { + this.mqttDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); + } + }); this.mqttDeviceProfileTransportConfigurationFormGroup.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(() => { @@ -135,6 +152,7 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control this.mqttDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); } else { this.mqttDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); + this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').updateValueAndValidity({onlySelf: true}); } } @@ -151,13 +169,44 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control if (isDefinedAndNotNull(value)) { this.mqttDeviceProfileTransportConfigurationFormGroup.patchValue(value, {emitEvent: false}); this.updateTransportPayloadBasedControls(value.transportPayloadTypeConfiguration?.transportPayloadType); + if (!this.disabled) { + this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').updateValueAndValidity({onlySelf: true}); + } + } + } + + removeAttributeMetricName(name: string): void { + const names: string[] = this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').value; + const index = names.indexOf(name); + if (index >= 0) { + names.splice(index, 1); + this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').setValue(names); + } + } + + addAttributeMetricName(event: MatChipInputEvent): void { + const input = event.input; + let value = event.value; + if ((value || '').trim()) { + value = value.trim(); + let names: string[] = this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').value; + if (!names || names.indexOf(value) === -1) { + if (!names) { + names = []; + } + names.push(value); + this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').setValue(names, {emitEvent: true}); + } + } + if (input) { + input.value = ''; } } private updateModel() { let configuration: DeviceProfileTransportConfiguration = null; if (this.mqttDeviceProfileTransportConfigurationFormGroup.valid) { - configuration = this.mqttDeviceProfileTransportConfigurationFormGroup.value; + configuration = this.mqttDeviceProfileTransportConfigurationFormGroup.getRawValue(); configuration.type = DeviceTransportType.MQTT; } this.propagateChange(configuration); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts index c0e5aa4212..aae7d2c37a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts @@ -341,6 +341,7 @@ export class TbFlot { private init($element: JQuery, subscription: IWidgetSubscription) { this.$element = $element; + this.$element.css('letter-spacing', 'normal'); this.subscription = subscription; this.comparisonEnabled = this.subscription ? this.subscription.comparisonEnabled : this.settings.comparisonEnabled; if (this.comparisonEnabled) { diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index e36aa99117..df7ba701c7 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -242,6 +242,7 @@ export interface DefaultDeviceProfileTransportConfiguration { export interface MqttDeviceProfileTransportConfiguration { deviceTelemetryTopic?: string; deviceAttributesTopic?: string; + sparkplug?: boolean; sendAckOnValidationException?: boolean; transportPayloadTypeConfiguration?: { transportPayloadType?: TransportPayloadType; @@ -359,6 +360,7 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT const mqttTransportConfiguration: MqttDeviceProfileTransportConfiguration = { deviceTelemetryTopic: 'v1/devices/me/telemetry', deviceAttributesTopic: 'v1/devices/me/attributes', + sparkplug: false, sendAckOnValidationException: false, transportPayloadTypeConfiguration: { transportPayloadType: TransportPayloadType.JSON, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index ceac31ef1d..2595622701 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1390,6 +1390,10 @@ "create-new-device-profile": "Create a new one!", "mqtt-device-topic-filters": "MQTT device topic filters", "mqtt-device-topic-filters-unique": "MQTT device topic filters need to be unique.", + "mqtt-device-topic-filters-spark-plug": "MQTT device topic filters SparkPlug.", + "mqtt-device-topic-filters-spark-plug-hint": "Default - telemetry. Example: namespace/group_id/message_type/edge_node_id/[device_id].", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "SparkPlug attributes metric names", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "Names of SparkPlug metrics that will be stored as device attributes. All other metrics will be stored as device telemetry", "mqtt-device-payload-type": "MQTT device payload", "mqtt-device-payload-type-json": "JSON", "mqtt-device-payload-type-proto": "Protobuf",