From 74daec474f96e6dd0b804d9e0ef1e1d1fb14a9df Mon Sep 17 00:00:00 2001 From: nick Date: Mon, 25 Dec 2023 22:09:39 +0200 Subject: [PATCH 01/23] lwm2ml: ver 3.9.1 - start --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 51 +-- .../DefaultLwM2mAttributeParserTest.java | 147 +++++++ .../DefaultLwM2mLinkParserTest.java | 76 ++++ .../lwm2m/attributes/LwM2mAttributesTest.java | 87 ++++ .../transport/lwm2m/client/FwLwM2MDevice.java | 20 +- .../lwm2m/client/LwM2MTestClient.java | 243 +++++++---- .../client/LwM2mBinaryAppDataContainer.java | 5 +- .../transport/lwm2m/client/LwM2mLocation.java | 3 +- .../lwm2m/client/LwM2mTemperatureSensor.java | 8 +- .../transport/lwm2m/client/Lwm2mServer.java | 11 +- .../lwm2m/client/SimpleLwM2MDevice.java | 17 +- .../transport/lwm2m/client/SwLwM2MDevice.java | 19 +- .../ota/sql/OtaLwM2MIntegrationTest.java | 12 +- .../rpc/AbstractRpcLwM2MIntegrationTest.java | 7 +- .../sql/RpcLwm2mIntegrationCreateTest.java | 4 +- .../sql/RpcLwm2mIntegrationDiscoverTest.java | 17 +- .../sql/RpcLwm2mIntegrationObserveTest.java | 4 +- .../rpc/sql/RpcLwm2mIntegrationReadTest.java | 36 +- ...pcLwm2mIntegrationWriteAttributesTest.java | 8 +- .../rpc/sql/RpcLwm2mIntegrationWriteTest.java | 67 +-- .../AbstractSecurityLwM2MIntegrationTest.java | 35 +- .../sql/NoSecLwM2MIntegrationTest.java | 18 +- .../security/sql/PskLwm2mIntegrationTest.java | 9 +- .../security/sql/RpkLwM2MIntegrationTest.java | 10 +- .../sql/X509_NoTrustLwM2MIntegrationTest.java | 6 +- .../sql/X509_TrustLwM2MIntegrationTest.java | 6 +- .../bootstrap/LwM2MServerSecurityConfig.java | 2 +- .../common/data/util/TbDDFFileParser.java | 6 +- .../data/util/TbDefaultDDFFileValidator.java | 129 ++++++ .../LwM2MTransportBootstrapService.java | 149 +++++-- .../LwM2mDefaultBootstrapSessionManager.java | 35 +- ...LwM2MBootstrapConfigStoreTaskProvider.java | 13 +- .../store/LwM2MBootstrapSecurityStore.java | 8 +- .../store/LwM2MConfigurationChecker.java | 22 +- .../LwM2MInMemoryBootstrapConfigStore.java | 6 +- ...LwM2mCredentialsSecurityInfoValidator.java | 4 +- .../lwm2m/secure/TbLwM2MAuthorizer.java | 38 +- .../AbstractLwM2mTransportResource.java | 3 +- .../server/DefaultLwM2mTransportService.java | 141 +++++-- .../lwm2m/server/LwM2MNetworkConfig.java | 6 +- .../lwm2m/server/LwM2mServerListener.java | 12 +- .../lwm2m/server/LwM2mTransportContext.java | 2 +- .../server/LwM2mVersionedModelProvider.java | 10 +- .../lwm2m/server/client/LwM2mClient.java | 21 +- .../DefaultLwM2mDownlinkMsgHandler.java | 88 ++-- .../downlink/LwM2mDownlinkMsgHandler.java | 8 +- .../rpc/DefaultLwM2MRpcRequestHandler.java | 115 +++--- .../lwm2m/server/rpc/RpcDiscoverCallback.java | 2 +- .../server/store/TbInMemorySecurityStore.java | 16 +- .../store/TbLwM2mRedisRegistrationStore.java | 99 +++-- .../store/TbLwM2mRedisSecurityStore.java | 22 +- .../server/store/TbLwM2mSecurityStore.java | 8 +- .../server/store/TbLwM2mStoreFactory.java | 6 +- .../server/store/util/LwM2MClientSerDes.java | 383 +++++++++--------- .../uplink/DefaultLwM2mUplinkMsgHandler.java | 4 +- .../lwm2m/utils/LwM2MTransportUtil.java | 133 +++--- .../validator/DeviceProfileDataValidator.java | 9 +- pom.xml | 10 +- .../lwm2m/lwm2m-profile-config.models.ts | 2 +- 59 files changed, 1629 insertions(+), 809 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mAttributeParserTest.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/util/TbDefaultDDFFileValidator.java 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 7d2bc51ce6..a160ba81f8 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 @@ -20,7 +20,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.eclipse.californium.elements.config.Configuration; -import org.eclipse.leshan.client.californium.LeshanClient; +import org.eclipse.leshan.client.LeshanClient; import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.core.ResponseCode; import org.junit.After; @@ -88,7 +88,6 @@ import static org.awaitility.Awaitility.await; import static org.eclipse.californium.core.config.CoapConfig.COAP_PORT; import static org.eclipse.californium.core.config.CoapConfig.COAP_SECURE_PORT; import static org.eclipse.leshan.client.object.Security.noSec; -import static org.eclipse.leshan.client.object.Security.noSecBootstap; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_STARTED; @@ -121,8 +120,11 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte public static final String host = "localhost"; public static final String hostBs = "localhost"; - public static final int shortServerId = 123; - public static final int shortServerIdBs = 111; + public static final Integer shortServerId = 123; + public static final Integer shortServerIdBs0 = 0; + public static final int serverId = 1; + public static final int serverIdBs = 0; + public static final String COAP = "coap://"; public static final String COAPS = "coaps://"; public static final String URI = COAP + host + ":" + port; @@ -132,7 +134,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte public static final Configuration COAP_CONFIG = new Configuration().set(COAP_PORT, port).set(COAP_SECURE_PORT, securityPort); public static Configuration COAP_CONFIG_BS = new Configuration().set(COAP_PORT, portBs).set(COAP_SECURE_PORT, securityPortBs); public static final Security SECURITY_NO_SEC = noSec(URI, shortServerId); - public static final Security SECURITY_NO_SEC_BS = noSecBootstap(URI_BS); protected final String OBSERVE_ATTRIBUTES_WITHOUT_PARAMS = " {\n" + @@ -235,8 +236,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte getWsClient().waitForReply(); getWsClient().registerWaitForUpdate(); - createNewClient(security, coapConfig, false, endpoint, false, null); - awaitObserveReadAll(0, false, device.getId().getId().toString()); + createNewClient(security, null, coapConfig, false, endpoint); + awaitObserveReadAll(0, device.getId().getId().toString()); String msg = getWsClient().waitForUpdate(); EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class); @@ -252,6 +253,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte int expectedMin = 5; Assert.assertTrue(expectedMax >= Long.parseLong(tsValue.getValue())); Assert.assertTrue(expectedMin <= Long.parseLong(tsValue.getValue())); + + } protected void createDeviceProfile(Lwm2mDeviceProfileTransportConfiguration transportConfiguration) throws Exception { @@ -300,14 +303,14 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte this.resources = resources; } - public void createNewClient(Security security, Configuration coapConfig, boolean isRpc, String endpoint, boolean isBootstrap, Security securityBs) throws Exception { + public void createNewClient(Security security, Security securityBs, Configuration coapConfig, boolean isRpc, String endpoint) throws Exception { this.clientDestroy(); lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint); try (ServerSocket socket = new ServerSocket(0)) { int clientPort = socket.getLocalPort(); - lwM2MTestClient.init(security, coapConfig, clientPort, isRpc, isBootstrap, this.shortServerId, this.shortServerIdBs, - securityBs, this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest); + lwM2MTestClient.init(security, securityBs, coapConfig, clientPort, isRpc, + this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest); } } @@ -354,7 +357,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte private AbstractLwM2MBootstrapServerCredential getBootstrapServerCredentialNoSec(boolean isBootstrap) { AbstractLwM2MBootstrapServerCredential bootstrapServerCredential = new NoSecLwM2MBootstrapServerCredential(); bootstrapServerCredential.setServerPublicKey(""); - bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs : shortServerId); + bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs0 : shortServerId); bootstrapServerCredential.setBootstrapServerIs(isBootstrap); bootstrapServerCredential.setHost(isBootstrap ? hostBs : host); bootstrapServerCredential.setPort(isBootstrap ? portBs : port); @@ -397,20 +400,18 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte .until(() -> leshanClient.getRegisteredServers().size() == 0); } - protected void awaitObserveReadAll(int cntObserve, boolean isBootstrap, String deviceIdStr) throws Exception { - if (!isBootstrap) { - await("ObserveReadAll after start client: countObserve " + cntObserve) - .atMost(40, TimeUnit.SECONDS) - .until(() -> { - String actualResultReadAll = sendObserve("ObserveReadAll", null, deviceIdStr); - ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); - Assert.assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); - String actualValuesReadAll = rpcActualResultReadAll.get("value").asText(); - log.warn("ObserveReadAll: [{}]", actualValuesReadAll); - int actualCntObserve = "[]".equals(actualValuesReadAll) ? 0 : actualValuesReadAll.split(",").length; - return cntObserve == actualCntObserve; - }); - } + protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception { + await("ObserveReadAll after start client: countObserve " + cntObserve) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + String actualResultReadAll = sendObserve("ObserveReadAll", null, deviceIdStr); + ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + Assert.assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); + String actualValuesReadAll = rpcActualResultReadAll.get("value").asText(); + log.warn("ObserveReadAll: [{}]", actualValuesReadAll); + int actualCntObserve = "[]".equals(actualValuesReadAll) ? 0 : actualValuesReadAll.split(",").length; + return cntObserve == actualCntObserve; + }); } protected String sendObserve(String method, String params, String deviceIdStr) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mAttributeParserTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mAttributeParserTest.java new file mode 100644 index 0000000000..0172710d0d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mAttributeParserTest.java @@ -0,0 +1,147 @@ +/** + * 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.lwm2m.attributes; + +import org.eclipse.leshan.core.link.LinkParseException; +import org.eclipse.leshan.core.link.attributes.InvalidAttributeException; +import org.eclipse.leshan.core.link.lwm2m.attributes.DefaultLwM2mAttributeParser; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DefaultLwM2mAttributeParserTest { + + private final DefaultLwM2mAttributeParser parser = new DefaultLwM2mAttributeParser(); + + private static Stream validAttributes() throws InvalidAttributeException { + return Stream.of(// + Arguments.of("pmin", LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD)), // + Arguments.of("pmin=30", LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 30l)), // + Arguments.of("pmax", LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD)), // + Arguments.of("pmax=60", LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 60l)), // + Arguments.of("dim=2", LwM2mAttributes.create(LwM2mAttributes.DIMENSION, 2l)), // + Arguments.of("epmin", LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD)), // + Arguments.of("epmin=30", LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD, 30l)), // + Arguments.of("epmax", LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD)), // + Arguments.of("epmax=60", LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD, 60l)), // + Arguments.of("lt", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN)), // + Arguments.of("lt=30", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 30d)), // + Arguments.of("lt=-30", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, -30d)), // + Arguments.of("lt=30.55", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 30.55)), // + Arguments.of("lt=-30.55", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, -30.55)), // + Arguments.of("gt", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN)), // + Arguments.of("gt=60", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 60d)), // + Arguments.of("gt=-60", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, -60d)), // + Arguments.of("gt=60.55", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 60.55)), // + Arguments.of("gt=-60.55", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, -60.55)), // + Arguments.of("st", LwM2mAttributes.create(LwM2mAttributes.STEP)), // + Arguments.of("st=60", LwM2mAttributes.create(LwM2mAttributes.STEP, 60d)), // + Arguments.of("st=60.55", LwM2mAttributes.create(LwM2mAttributes.STEP, 60.55)) // + ); + } + + @ParameterizedTest(name = "Tests {index} : {0}") + @MethodSource("validAttributes") + public void check_one_valid_attribute(String attributeAsString, LwM2mAttribute expectedValue) + throws LinkParseException, InvalidAttributeException { + LwM2mAttributeSet parsed = new LwM2mAttributeSet(parser.parseUriQuery(attributeAsString)); + LwM2mAttributeSet attResult = new LwM2mAttributeSet(expectedValue); + assertEquals(parsed, attResult); + } + + private static String[] invalidAttributes() { + return new String[] { // + // MINIMUM_PERIOD + "pmin=", // + "pmin=1.9", // + "pmin=-1.8", // + "pmin=a", // + // MAXIMUM_PERIOD + "pmax=", // + "pmax=-2", // + "pmax=2.7", // + "pmax=-2.6", // + "pmax=bc", // + // DIMENSION + "dim", // + "dim=", // + "dim=-3", // + "dim=3.5", // + "dim=-3.4", // + "dim=def", // + // EVALUATE_MINIMUM_PERIOD + "epmin=", // + "epmin=-4", // + "epmin=4.3", // + "epmin=-4.2", // + "epmin=ghij", // + // EVALUATE_MAXIMUM_PERIOD + "epmax=", // + "epmax=-5", // + "epmax=5.1", // + "epmax=-5.9", // + "epmax=klmno", // + // LESSER_THAN + "lt=", // + "lt=pqrts", // + "lt=0abc", // + // GREATER_THAN + "gt=", // + "gt=uvwxyz", // + "gt=0.xyz", // + // STEP + "st=", // + "st=-6", // + "st=-6.8", // + // Multi-attributes + "&pmin&pmax=60>=2&", // + "&pmin&pmax=60>=2", // + "pmin&pmax=60>=2&", // + "pmin&pmax=60&>=2", // + "pmin&pmax=60&>=2&&&", // + "&&&pmin&pmax=60&>=2", // + }; + } + + @ParameterizedTest(name = "Test {index} : {0}") + @MethodSource("invalidAttributes") + public void check_invalid_attributes(String invalidAttributesAsString) { + assertThrows(InvalidAttributeException.class, + () -> new LwM2mAttributeSet(parser.parseUriQuery(invalidAttributesAsString))); + + } + + @Test + public void check_multiple_valid_attributes() throws LinkParseException, InvalidAttributeException { + + LwM2mAttributeSet parsed = new LwM2mAttributeSet(parser.parseUriQuery("pmin&pmax=60>=2")); + LwM2mAttributeSet attResult = new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD), // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 60l), // + LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 2d)); + + assertEquals(attResult, parsed); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java new file mode 100644 index 0000000000..b5d8fecf10 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java @@ -0,0 +1,76 @@ +/** + * 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.lwm2m.attributes; + +import org.eclipse.leshan.core.link.LinkParseException; +import org.eclipse.leshan.core.link.attributes.AttributeSet; +import org.eclipse.leshan.core.link.lwm2m.DefaultLwM2mLinkParser; +import org.eclipse.leshan.core.link.lwm2m.LwM2mLink; +import org.eclipse.leshan.core.link.lwm2m.LwM2mLinkParser; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +public class DefaultLwM2mLinkParserTest { + + private final LwM2mLinkParser parser = new DefaultLwM2mLinkParser(); + + @Test + public void check_invalid_values() throws LinkParseException { + // first check it's OK with valid value + LwM2mLink[] parsed = parser.parseLwM2mLinkFromCoreLinkFormat(";dim=255".getBytes(), null); + assertEquals(new LwM2mPath(3, 0, 11), parsed[0].getPath()); + AttributeSet attResult = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.DIMENSION, 255l)); + assertEquals(attResult, parsed[0].getAttributes()); + + // then check an invalid one + assertThrowsExactly(LinkParseException.class, () -> { + // dim should be between 0-255 + parser.parseLwM2mLinkFromCoreLinkFormat(";dim=256".getBytes(), null); + }); + + // first check it's OK with valid value + parsed = parser.parseLwM2mLinkFromCoreLinkFormat(";ssid=1".getBytes(), null); + assertEquals(new LwM2mPath(0, 1), parsed[0].getPath()); + attResult = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.SHORT_SERVER_ID, 1l)); + assertEquals(attResult, parsed[0].getAttributes()); + + // then check an invalid one + assertThrowsExactly(LinkParseException.class, () -> { + // ssid should be between 1-65534 + parser.parseLwM2mLinkFromCoreLinkFormat(";ssid=0".getBytes(), null); + }); + } + + @Test + public void check_attribute_with_no_value_failed() throws LinkParseException { + // first check it's OK with value + LwM2mLink[] parsed = parser.parseLwM2mLinkFromCoreLinkFormat(";pmin=200".getBytes(), null); + assertEquals(new LwM2mPath(3, 0, 11), parsed[0].getPath()); + AttributeSet attResult = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 200l)); + assertEquals(attResult, parsed[0].getAttributes()); + + // then check an invalid one + assertThrowsExactly(LinkParseException.class, () -> { + // dim should be between 0-255 + parser.parseLwM2mLinkFromCoreLinkFormat(";pmin".getBytes(), null); + }); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java new file mode 100644 index 0000000000..d77a3252c0 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java @@ -0,0 +1,87 @@ +/** + * 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.lwm2m.attributes; + +import org.eclipse.leshan.core.link.attributes.InvalidAttributeException; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeModel; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class LwM2mAttributesTest { + + @ParameterizedTest(name = "Tests {index} : {0}") + @MethodSource("supportNullAttributes") + public void check_attribute_can_be_created_with_null_value(LwM2mAttributeModel model) + throws InvalidAttributeException { + LwM2mAttribute attribute = LwM2mAttributes.create(model); + assertNotNull(attribute); + assertFalse(attribute.hasValue()); + assertNull(attribute.getValue()); + attribute = LwM2mAttributes.create(model, null); + assertNotNull(attribute); + assertFalse(attribute.hasValue()); + assertNull(attribute.getValue()); + } + + @ParameterizedTest(name = "Tests {index} : {0}") + @MethodSource("doesntSupportAttributesWithoutValue") + public void check_attribute_can_not_be_created_without_value(LwM2mAttributeModel model) { + assertThrows(UnsupportedOperationException.class, () -> LwM2mAttributes.create(model)); + } + + @ParameterizedTest(name = "Tests {index} : {0}") + @MethodSource("doesntSupportAttributesWithValueNull") + public void check_attribute_can_not_be_created_with_null(LwM2mAttributeModel model) { + assertThrows(NullPointerException.class, () -> LwM2mAttributes.create(model, null)); + } + + private static Stream supportNullAttributes() throws InvalidAttributeException { + return Stream.of(// + Arguments.of(LwM2mAttributes.MINIMUM_PERIOD), // + Arguments.of(LwM2mAttributes.MAXIMUM_PERIOD), // + Arguments.of(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD), // + Arguments.of(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD), // + Arguments.of(LwM2mAttributes.LESSER_THAN), // + Arguments.of(LwM2mAttributes.GREATER_THAN), // + Arguments.of(LwM2mAttributes.STEP) // + ); + } + + private static Stream doesntSupportAttributesWithoutValue() throws InvalidAttributeException { + return Stream.of(// + Arguments.of(LwM2mAttributes.ENABLER_VERSION), // + Arguments.of(LwM2mAttributes.OBJECT_VERSION)// + ); + } + + private static Stream doesntSupportAttributesWithValueNull() throws InvalidAttributeException { + return Stream.of(// + Arguments.of(LwM2mAttributes.DIMENSION), // + Arguments.of(LwM2mAttributes.SHORT_SERVER_ID) // + ); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java index 5fb21b344a..86124ccb5f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java @@ -17,9 +17,9 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; -import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.request.argument.Arguments; import org.eclipse.leshan.core.response.ExecuteResponse; import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.core.response.WriteResponse; @@ -45,7 +45,7 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { private final AtomicInteger updateResult = new AtomicInteger(0); @Override - public ReadResponse read(ServerIdentity identity, int resourceId) { + public ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId) { if (!identity.isSystem()) log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); switch (resourceId) { @@ -65,24 +65,24 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { } @Override - public ExecuteResponse execute(ServerIdentity identity, int resourceId, String params) { - String withParams = null; - if (params != null && params.length() != 0) { - withParams = " with params " + params; - } - log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withParams != null ? withParams : ""); + public ExecuteResponse execute(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId, Arguments arguments) { + String withArguments = ""; + if (!arguments.isEmpty()) + withArguments = " with arguments " + arguments; + log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments); + switch (resourceId) { case 2: startUpdating(); return ExecuteResponse.success(); default: - return super.execute(identity, resourceId, params); + return super.execute(identity, resourceId, arguments); } } @Override - public WriteResponse write(ServerIdentity identity, boolean replace, int resourceId, LwM2mResource value) { + public WriteResponse write(org.eclipse.leshan.client.servers.LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); switch (resourceId) { 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 449ac4c2ae..6e6fa12cf7 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 @@ -18,17 +18,28 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.scandium.config.DtlsConfig; import org.eclipse.californium.scandium.config.DtlsConnectorConfig; -import org.eclipse.leshan.client.californium.LeshanClient; -import org.eclipse.leshan.client.californium.LeshanClientBuilder; +import org.eclipse.leshan.client.LeshanClient; +import org.eclipse.leshan.client.LeshanClientBuilder; +import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointFactory; +import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointsProvider; +import org.eclipse.leshan.client.californium.endpoint.ClientProtocolProvider; +import org.eclipse.leshan.client.californium.endpoint.coap.CoapOscoreProtocolProvider; +import org.eclipse.leshan.client.californium.endpoint.coaps.CoapsClientEndpointFactory; +import org.eclipse.leshan.client.californium.endpoint.coaps.CoapsClientProtocolProvider; +import org.eclipse.leshan.client.endpoint.LwM2mClientEndpointsProvider; import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory; import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.client.object.Server; import org.eclipse.leshan.client.observer.LwM2mClientObserver; import org.eclipse.leshan.client.resource.DummyInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; import org.eclipse.leshan.client.resource.ObjectsInitializer; -import org.eclipse.leshan.client.servers.ServerIdentity; +import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter; +import org.eclipse.leshan.client.send.ManualDataSender; +import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.model.InvalidDDFFileException; import org.eclipse.leshan.core.model.LwM2mModel; @@ -49,13 +60,13 @@ import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandle import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import java.io.IOException; +import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; -import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.eclipse.leshan.core.LwM2mId.DEVICE; import static org.eclipse.leshan.core.LwM2mId.FIRMWARE; @@ -63,6 +74,10 @@ import static org.eclipse.leshan.core.LwM2mId.LOCATION; import static org.eclipse.leshan.core.LwM2mId.SECURITY; import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; +import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverId; +import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverIdBs; +import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerId; +import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerIdBs0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_FAILURE; @@ -97,11 +112,6 @@ public class LwM2MTestClient { private final ScheduledExecutorService executor; private final String endpoint; private LeshanClient leshanClient; - - private Security lwm2mSecurity; - private Security lwm2mSecurityBs; - private Lwm2mServer lwm2mServer; - private Lwm2mServer lwm2mServerBs; private SimpleLwM2MDevice lwM2MDevice; private FwLwM2MDevice fwLwM2MDevice; private SwLwM2MDevice swLwM2MDevice; @@ -112,8 +122,7 @@ public class LwM2MTestClient { 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, + public void init(Security security, Security securityBs,Configuration coapConfig, int port, boolean isRpc, LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mClientContext clientContext) throws InvalidDDFFileException, IOException { Assert.assertNull("client already initialized", leshanClient); @@ -125,30 +134,36 @@ public class LwM2MTestClient { } LwM2mModel model = new StaticModel(models); ObjectsInitializer initializer = new ObjectsInitializer(model); - if (securityBs == null) { - initializer.setInstancesForObject(SECURITY, this.lwm2mSecurity = security); - } else { - securityBs.setId(0); - security.setId(1); - LwM2mInstanceEnabler[] instances = new LwM2mInstanceEnabler[]{this.lwm2mSecurityBs = securityBs, this.lwm2mSecurity = security}; + + if (securityBs != null && security != null) { + // SECURITY + security.setId(serverId); + securityBs.setId(serverIdBs); + LwM2mInstanceEnabler[] instances = new LwM2mInstanceEnabler[]{securityBs, security}; initializer.setClassForObject(SECURITY, Security.class); initializer.setInstancesForObject(SECURITY, instances); - } - if (isBootstrap) { - initializer.setInstancesForObject(SERVER, lwm2mServerBs = new Lwm2mServer(shortServerIdBs, 300)); + // SERVER + Server lwm2mServer = new Server(shortServerId, 300); + lwm2mServer.setId(serverId); + Server serverBs = new Server(shortServerIdBs0, 300); + serverBs.setId(serverIdBs); + instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer}; + initializer.setClassForObject(SERVER, Server.class); + initializer.setInstancesForObject(SERVER, instances); + } else if (securityBs != null) { + // SECURITY + initializer.setInstancesForObject(SECURITY, securityBs); + // SERVER + initializer.setClassForObject(SERVER, Server.class); } else { - if (securityBs == null) { - initializer.setInstancesForObject(SERVER, lwm2mServer = new Lwm2mServer(shortServerId, 300)); - } else { - lwm2mServerBs = new Lwm2mServer(shortServerIdBs, 300); - lwm2mServerBs.setId(0); - lwm2mServer = new Lwm2mServer(shortServerId, 300); - lwm2mServer.setId(1); - LwM2mInstanceEnabler[] instances = new LwM2mInstanceEnabler[]{lwm2mServerBs, lwm2mServer}; - initializer.setClassForObject(SERVER, Server.class); - initializer.setInstancesForObject(SERVER, instances); - } + // SECURITY + initializer.setInstancesForObject(SECURITY, security); + // SERVER + Server lwm2mServer = new Server(shortServerId, 300); + lwm2mServer.setId(serverId); + initializer.setInstancesForObject(SERVER, lwm2mServer ); } + initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor)); initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice()); initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice()); @@ -160,106 +175,193 @@ public class LwM2MTestClient { initializer.setInstancesForObject(LOCATION, new LwM2mLocation(locationParams.getLatitude(), locationParams.getLongitude(), locationParams.getScaleFactor(), executor, OBJECT_INSTANCE_ID_0)); initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2MTemperatureSensor = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_0), new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12)); + List enablers = initializer.createAll(); + + // Create Californium Endpoints Provider: + // -------------------------------------- + // Define Custom CoAPS protocol provider + CoapsClientProtocolProvider customCoapsProtocolProvider = new CoapsClientProtocolProvider() { + @Override + public CaliforniumClientEndpointFactory createDefaultEndpointFactory() { + return new CoapsClientEndpointFactory() { + + @Override + protected DtlsConnectorConfig.Builder createRootDtlsConnectorConfigBuilder( + Configuration configuration) { + DtlsConnectorConfig.Builder builder = super.createRootDtlsConnectorConfigBuilder(configuration); + return builder; + }; + }; + } + }; + + // Create client endpoints Provider + List protocolProvider = new ArrayList<>(); + + /** + * "Use java-coap for CoAP protocol instead of Californium." + */ + boolean useJavaCoap = false; + if (!useJavaCoap) protocolProvider.add(new CoapOscoreProtocolProvider()); + protocolProvider.add(customCoapsProtocolProvider); + CaliforniumClientEndpointsProvider.Builder endpointsBuilder = new CaliforniumClientEndpointsProvider.Builder( + protocolProvider.toArray(new ClientProtocolProvider[protocolProvider.size()])); + + + // Create Californium Configuration + Configuration clientCoapConfig = endpointsBuilder.createDefaultConfiguration(); DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(coapConfig); - dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, true); + // Set some DTLS stuff + // These configuration values are always overwritten by CLI therefore set them to transient. + clientCoapConfig.setTransient(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY); + clientCoapConfig.setTransient(DtlsConfig.DTLS_CONNECTION_ID_LENGTH); + boolean supportDeprecatedCiphers = false; + clientCoapConfig.set(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, !supportDeprecatedCiphers); + /** + * "Control usage of DTLS connection ID.", // + * "- 'on' to activate Connection ID support (same as -cid 0)", // + * "- 'off' to deactivate it", // + * "- Positive value define the size in byte of CID generated.", // + * "- 0 value means we accept to use CID but will not generated one for foreign peer.", // + * "Default: off" + */ + Integer cid = null; + clientCoapConfig.set(DtlsConfig.DTLS_CONNECTION_ID_LENGTH, cid); + + // Set Californium Configuration + endpointsBuilder.setConfiguration(clientCoapConfig); + endpointsBuilder.setClientAddress(new InetSocketAddress(port).getAddress()); + + + // creates EndpointsProvider + List endpointsProvider = new ArrayList<>(); + endpointsProvider.add(endpointsBuilder.build()); + /** + * dependency -> org.eclipse.leshan.transport.javacoap.client.endpoint; + */ +// if (useJavaCoap) {endpointsProvider.add(new JavaCoapClientEndpointsProvider()); + + // Configure Registration Engine DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory(); - engineFactory.setReconnectOnUpdate(false); + // old + /** + * Force reconnection/rehandshake on registration update. + */ + int comPeriodInSec = 5; + if (comPeriodInSec > 0) engineFactory.setCommunicationPeriod(comPeriodInSec * 1000); +// engineFactory.setCommunicationPeriod(5000); // old + /** + * By default client will try to resume DTLS session by using abbreviated Handshake. This option force to always do a full handshake." + */ + boolean reconnectOnUpdate = false; + engineFactory.setReconnectOnUpdate(reconnectOnUpdate); +// engineFactory.setReconnectOnUpdate(false); // old engineFactory.setResumeOnConnect(true); - engineFactory.setCommunicationPeriod(5000); - + // new + /** + * Client use queue mode (not fully implemented). + */ + boolean queueMode = false; + engineFactory.setQueueMode(queueMode); + + // Create client LeshanClientBuilder builder = new LeshanClientBuilder(endpoint); - builder.setLocalAddress("0.0.0.0", port); - builder.setObjects(initializer.createAll()); - builder.setCoapConfig(coapConfig); - builder.setDtlsConfig(dtlsConfig); + builder.setObjects(enablers); + builder.setEndpointsProviders(endpointsProvider.toArray(new LwM2mClientEndpointsProvider[endpointsProvider.size()])); + builder.setDataSenders(new ManualDataSender()); + builder.setRegistrationEngineFactory(engineFactory); + boolean supportOldFormat = true; + if (supportOldFormat) { + builder.setDecoder(new DefaultLwM2mDecoder(supportOldFormat)); + builder.setEncoder(new DefaultLwM2mEncoder(new LwM2mValueConverterImpl(), supportOldFormat)); + } + builder.setRegistrationEngineFactory(engineFactory); builder.setSharedExecutor(executor); - builder.setDecoder(new DefaultLwM2mDecoder(false)); - builder.setEncoder(new DefaultLwM2mEncoder(new LwM2mValueConverterImpl(), false)); clientStates = new HashSet<>(); clientStates.add(ON_INIT); leshanClient = builder.build(); LwM2mClientObserver observer = new LwM2mClientObserver() { @Override - public void onBootstrapStarted(ServerIdentity bsserver, BootstrapRequest request) { + public void onBootstrapStarted(LwM2mServer bsserver, BootstrapRequest request) { clientStates.add(ON_BOOTSTRAP_STARTED); } @Override - public void onBootstrapSuccess(ServerIdentity bsserver, BootstrapRequest request) { + public void onBootstrapSuccess(LwM2mServer bsserver, BootstrapRequest request) { clientStates.add(ON_BOOTSTRAP_SUCCESS); } @Override - public void onBootstrapFailure(ServerIdentity bsserver, BootstrapRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + public void onBootstrapFailure(LwM2mServer bsserver, BootstrapRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { clientStates.add(ON_BOOTSTRAP_FAILURE); } @Override - public void onBootstrapTimeout(ServerIdentity bsserver, BootstrapRequest request) { + public void onBootstrapTimeout(LwM2mServer bsserver, BootstrapRequest request) { clientStates.add(ON_BOOTSTRAP_TIMEOUT); } @Override - public void onRegistrationStarted(ServerIdentity server, RegisterRequest request) { + public void onRegistrationStarted(LwM2mServer server, RegisterRequest request) { clientStates.add(ON_REGISTRATION_STARTED); } @Override - public void onRegistrationSuccess(ServerIdentity server, RegisterRequest request, String registrationID) { + public void onRegistrationSuccess(LwM2mServer server, RegisterRequest request, String registrationID) { clientStates.add(ON_REGISTRATION_SUCCESS); } @Override - public void onRegistrationFailure(ServerIdentity server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + public void onRegistrationFailure(LwM2mServer server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { clientStates.add(ON_REGISTRATION_FAILURE); } @Override - public void onRegistrationTimeout(ServerIdentity server, RegisterRequest request) { + public void onRegistrationTimeout(LwM2mServer server, RegisterRequest request) { clientStates.add(ON_REGISTRATION_TIMEOUT); } @Override - public void onUpdateStarted(ServerIdentity server, UpdateRequest request) { + public void onUpdateStarted(LwM2mServer server, UpdateRequest request) { clientStates.add(ON_UPDATE_STARTED); } @Override - public void onUpdateSuccess(ServerIdentity server, UpdateRequest request) { + public void onUpdateSuccess(LwM2mServer server, UpdateRequest request) { clientStates.add(ON_UPDATE_SUCCESS); } @Override - public void onUpdateFailure(ServerIdentity server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + public void onUpdateFailure(LwM2mServer server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { clientStates.add(ON_UPDATE_FAILURE); } @Override - public void onUpdateTimeout(ServerIdentity server, UpdateRequest request) { + public void onUpdateTimeout(LwM2mServer server, UpdateRequest request) { clientStates.add(ON_UPDATE_TIMEOUT); } @Override - public void onDeregistrationStarted(ServerIdentity server, DeregisterRequest request) { + public void onDeregistrationStarted(LwM2mServer server, DeregisterRequest request) { clientStates.add(ON_DEREGISTRATION_STARTED); } @Override - public void onDeregistrationSuccess(ServerIdentity server, DeregisterRequest request) { + public void onDeregistrationSuccess(LwM2mServer server, DeregisterRequest request) { clientStates.add(ON_DEREGISTRATION_SUCCESS); } @Override - public void onDeregistrationFailure(ServerIdentity server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + public void onDeregistrationFailure(LwM2mServer server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { clientStates.add(ON_DEREGISTRATION_FAILURE); } @Override - public void onDeregistrationTimeout(ServerIdentity server, DeregisterRequest request) { + public void onDeregistrationTimeout(LwM2mServer server, DeregisterRequest request) { clientStates.add(ON_DEREGISTRATION_TIMEOUT); } @@ -270,6 +372,21 @@ public class LwM2MTestClient { }; this.leshanClient.addObserver(observer); + // Add some log about object tree life cycle. + this.leshanClient.getObjectTree().addListener(new ObjectsListenerAdapter() { + + @Override + public void objectRemoved(LwM2mObjectEnabler object) { + log.info("Object {} v{} disabled.", object.getId(), object.getObjectModel().version); + } + + @Override + public void objectAdded(LwM2mObjectEnabler object) { + log.info("Object {} v{} enabled.", object.getId(), object.getObjectModel().version); + } + }); + + if (!isRpc) { this.start(true); } @@ -279,18 +396,6 @@ public class LwM2MTestClient { if (leshanClient != null) { leshanClient.destroy(true); } - if (lwm2mSecurityBs != null) { - lwm2mSecurityBs = null; - } - if (lwm2mSecurity != null) { - lwm2mSecurity = null; - } - if (lwm2mServerBs != null) { - lwm2mServerBs = null; - } - if (lwm2mServer != null) { - lwm2mServer = null; - } if (lwM2MDevice != null) { lwM2MDevice.destroy(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java index 12a29ab216..35866dea0f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java @@ -17,7 +17,6 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; -import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; @@ -93,7 +92,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements } @Override - public ReadResponse read(ServerIdentity identity, int resourceId) { + public ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId) { try { switch (resourceId) { case 0: @@ -118,7 +117,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements } @Override - public WriteResponse write(ServerIdentity identity, boolean replace, int resourceId, LwM2mResource value) { + public WriteResponse write(org.eclipse.leshan.client.servers.LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { log.info("Write on Device resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); switch (resourceId) { case 0: diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java index 48cba35e36..4599f7c0ee 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java @@ -17,7 +17,6 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; -import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.response.ReadResponse; @@ -85,7 +84,7 @@ public class LwM2mLocation extends BaseInstanceEnabler implements Destroyable { } @Override - public ReadResponse read(ServerIdentity identity, int resourceId) { + public ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId) { log.info("Read on Location resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); switch (resourceId) { case 0: diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java index 7836bbf881..ae0297e02b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java @@ -17,8 +17,8 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; -import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.request.argument.Arguments; import org.eclipse.leshan.core.response.ExecuteResponse; import org.eclipse.leshan.core.response.ReadResponse; @@ -56,7 +56,7 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr } @Override - public synchronized ReadResponse read(ServerIdentity identity, int resourceId) { + public synchronized ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId) { log.info("Read on Temperature resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); switch (resourceId) { case 5601: @@ -73,14 +73,14 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr } @Override - public synchronized ExecuteResponse execute(ServerIdentity identity, int resourceId, String params) { + public synchronized ExecuteResponse execute(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId, Arguments arguments) { log.info("Execute on Temperature resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); switch (resourceId) { case 5605: resetMinMaxMeasuredValues(); return ExecuteResponse.success(); default: - return super.execute(identity, resourceId, params); + return super.execute(identity, resourceId, arguments); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/Lwm2mServer.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/Lwm2mServer.java index 86f6f63a62..1cddbb17f8 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/Lwm2mServer.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/Lwm2mServer.java @@ -18,11 +18,11 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; -import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel.Type; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.request.BindingMode; +import org.eclipse.leshan.core.request.argument.Arguments; import org.eclipse.leshan.core.response.ExecuteResponse; import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.core.response.WriteResponse; @@ -70,7 +70,7 @@ public class Lwm2mServer extends BaseInstanceEnabler { } @Override - public ReadResponse read(ServerIdentity identity, int resourceid) { + public ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceid) { if (!identity.isSystem()) LOG.debug("Read on Server resource /{}/{}/{}", getModel().id, getId(), resourceid); @@ -108,7 +108,7 @@ public class Lwm2mServer extends BaseInstanceEnabler { } @Override - public WriteResponse write(ServerIdentity identity, boolean replace, int resourceid, LwM2mResource value) { + public WriteResponse write(org.eclipse.leshan.client.servers.LwM2mServer identity, boolean replace, int resourceid, LwM2mResource value) { if (!identity.isSystem()) log.debug("Write on Server resource /{}/{}/{}", getModel().id, getId(), resourceid); @@ -195,8 +195,7 @@ public class Lwm2mServer extends BaseInstanceEnabler { } } - @Override - public ExecuteResponse execute(ServerIdentity identity, int resourceid, String params) { + public ExecuteResponse execute(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceid, Arguments arguments) { log.info("Execute on Server resource /{}/{}/{}", getModel().id, getId(), resourceid); if (resourceid == 8) { getLwM2mClient().triggerRegistrationUpdate(identity); @@ -210,7 +209,7 @@ public class Lwm2mServer extends BaseInstanceEnabler { return ExecuteResponse.badRequest("probably no bootstrap server configured"); } } else { - return super.execute(identity, resourceid, params); + return super.execute(identity, resourceid, arguments); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java index dbb75c01cd..8a32092572 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java @@ -17,11 +17,11 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; -import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.request.argument.Arguments; import org.eclipse.leshan.core.response.ExecuteResponse; import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.core.response.WriteResponse; @@ -66,7 +66,7 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl @Override - public ReadResponse read(ServerIdentity identity, int resourceId) { + public ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId) { if (!identity.isSystem()) log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); switch (resourceId) { @@ -108,17 +108,16 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl } @Override - public ExecuteResponse execute(ServerIdentity identity, int resourceId, String params) { - String withParams = null; - if (params != null && params.length() != 0) { - withParams = " with params " + params; - } - log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withParams != null ? withParams : ""); + public ExecuteResponse execute(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId, Arguments arguments) { + String withArguments = ""; + if (!arguments.isEmpty()) + withArguments = " with arguments " + arguments; + log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments); return ExecuteResponse.success(); } @Override - public WriteResponse write(ServerIdentity identity, boolean replace, int resourceId, LwM2mResource value) { + public WriteResponse write(org.eclipse.leshan.client.servers.LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); switch (resourceId) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SwLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SwLwM2MDevice.java index 2be8c2d2ae..340f9ffcb2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SwLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SwLwM2MDevice.java @@ -17,9 +17,9 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; -import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.request.argument.Arguments; import org.eclipse.leshan.core.response.ExecuteResponse; import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.core.response.WriteResponse; @@ -45,7 +45,7 @@ public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { private final AtomicInteger updateResult = new AtomicInteger(0); @Override - public ReadResponse read(ServerIdentity identity, int resourceId) { + public ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId) { if (!identity.isSystem()) log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); switch (resourceId) { @@ -63,12 +63,11 @@ public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { } @Override - public ExecuteResponse execute(ServerIdentity identity, int resourceId, String params) { - String withParams = null; - if (params != null && params.length() != 0) { - withParams = " with params " + params; - } - log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withParams != null ? withParams : ""); + public ExecuteResponse execute(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId, Arguments arguments) { + String withArguments = ""; + if (!arguments.isEmpty()) + withArguments = " with arguments " + arguments; + log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceId, withArguments); switch (resourceId) { case 4: @@ -77,12 +76,12 @@ public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { case 6: return ExecuteResponse.success(); default: - return super.execute(identity, resourceId, params); + return super.execute(identity, resourceId, arguments); } } @Override - public WriteResponse write(ServerIdentity identity, boolean replace, int resourceId, LwM2mResource value) { + public WriteResponse write(org.eclipse.leshan.client.servers.LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); switch (resourceId) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index 5c580adc85..6e6b0f2c2a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java @@ -99,8 +99,8 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { createDeviceProfile(transportConfiguration); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_WITHOUT_FW_INFO)); final Device device = createDevice(deviceCredentials, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); - createNewClient(SECURITY_NO_SEC, COAP_CONFIG, false, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO, false, null); - awaitObserveReadAll(0, false, device.getId().getId().toString()); + createNewClient(SECURITY_NO_SEC, null, COAP_CONFIG, false, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); + awaitObserveReadAll(0, device.getId().getId().toString()); device.setFirmwareId(createFirmware().getId()); final Device savedDevice = doPost("/api/device", device, Device.class); @@ -124,8 +124,8 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { createDeviceProfile(transportConfiguration); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA5)); final Device device = createDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA5); - createNewClient(SECURITY_NO_SEC, COAP_CONFIG, false, this.CLIENT_ENDPOINT_OTA5, false, null); - awaitObserveReadAll(9, false, device.getId().getId().toString()); + createNewClient(SECURITY_NO_SEC, null, COAP_CONFIG, false, this.CLIENT_ENDPOINT_OTA5); + awaitObserveReadAll(9, device.getId().getId().toString()); device.setFirmwareId(createFirmware().getId()); final Device savedDevice = doPost("/api/device", device, Device.class); @@ -154,8 +154,8 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { createDeviceProfile(transportConfiguration); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA9)); final Device device = createDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA9); - createNewClient(SECURITY_NO_SEC, COAP_CONFIG, false, this.CLIENT_ENDPOINT_OTA9, false, null); - awaitObserveReadAll(9, false, device.getId().getId().toString()); + createNewClient(SECURITY_NO_SEC, null, COAP_CONFIG, false, this.CLIENT_ENDPOINT_OTA9); + awaitObserveReadAll(9, device.getId().getId().toString()); device.setSoftwareId(createSoftware().getId()); final Device savedDevice = doPost("/api/device", device, Device.class); //sync call diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index 2e51c300ee..ddd1329254 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.transport.lwm2m.rpc; +import org.eclipse.leshan.core.link.LinkParser; +import org.eclipse.leshan.core.link.lwm2m.DefaultLwM2mLinkParser; import org.junit.Before; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; @@ -50,6 +52,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources; @DaoSqlTest public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { + protected final LinkParser linkParser = new DefaultLwM2mLinkParser(); protected String OBSERVE_ATTRIBUTES_WITH_PARAMS_RPC; protected String deviceId; public Set expectedObjects; @@ -84,7 +87,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg private void initRpc () throws Exception { String endpoint = DEVICE_ENDPOINT_RPC_PREF + endpointSequence.incrementAndGet(); - createNewClient(SECURITY_NO_SEC, COAP_CONFIG, true, endpoint, false, null); + createNewClient(SECURITY_NO_SEC, null, COAP_CONFIG, true, endpoint); expectedObjects = ConcurrentHashMap.newKeySet(); expectedObjectIdVers = ConcurrentHashMap.newKeySet(); expectedInstances = ConcurrentHashMap.newKeySet(); @@ -147,7 +150,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg deviceId = device.getId().getId().toString(); lwM2MTestClient.start(true); - awaitObserveReadAll(2, false, device.getId().getId().toString()); +// awaitObserveReadAll(2, true, device.getId().getId().toString()); } protected String pathIdVerToObjectId(String pathIdVer) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java index 6e005fcaf8..3eb0d25e85 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java @@ -99,7 +99,7 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe String actualResult = sendRPCreateById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); - String expectedObjectId = pathIdVerToObjectId((String) expectedPath); + String expectedObjectId = pathIdVerToObjectId(expectedPath); LwM2mPath expectedPathId = new LwM2mPath(expectedObjectId); String expected = "Specified object id " + expectedPathId.getObjectId() + " absent in the list supported objects of the client or is security object!"; String actual = rpcActualResult.get("error").asText(); @@ -118,7 +118,7 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe String actualResult = sendRPCreateById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); - String expectedObjectId = pathIdVerToObjectId((String) expectedPath); + String expectedObjectId = pathIdVerToObjectId(expectedPath); LwM2mPath expectedPathId = new LwM2mPath(expectedObjectId); String expected = "Specified object id " + expectedPathId.getObjectId() + " absent in the list supported objects of the client or is security object!"; String actual = rpcActualResult.get("error").asText(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java index 7d3cf6acd1..ea0a1ad4ca 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java @@ -18,6 +18,8 @@ package org.thingsboard.server.transport.lwm2m.rpc.sql; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.link.Link; +import org.eclipse.leshan.core.link.LinkParseException; import org.eclipse.leshan.core.node.LwM2mPath; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; @@ -52,12 +54,17 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration Set actualObjects = ConcurrentHashMap.newKeySet(); Set actualInstances = ConcurrentHashMap.newKeySet(); rpcActualValue.forEach(node -> { - if (!node.get("uriReference").asText().equals("/")) { - LwM2mPath path = new LwM2mPath(node.get("uriReference").asText()); - actualObjects.add("/" + path.getObjectId()); - if (path.isObjectInstance()) { - actualInstances.add("/" + path.getObjectId() + "/" + path.getObjectInstanceId()); + try { + Link[] parsedLink = linkParser.parseCoreLinkFormat(node.asText().getBytes()); + if (!parsedLink[0].getUriReference().equals("/")) { + LwM2mPath path = new LwM2mPath(parsedLink[0].getUriReference()); + actualObjects.add("/" + path.getObjectId()); + if (path.isObjectInstance()) { + actualInstances.add("/" + path.getObjectId() + "/" + path.getObjectInstanceId()); + } } + } catch (LinkParseException e) { + throw new RuntimeException(e); } }); assertEquals(expectedInstances, actualInstances); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index a54fdce6b5..9a17978e83 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -168,12 +168,14 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testObserveReadAll_Result_CONTENT_Value_Contains_Paths_Count_ObserveReadAll() throws Exception { + String expectedId = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; + sendRpcObserve("Observe", expectedId); String actualResultReadAll = sendRpcObserve("ObserveReadAll", null); ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); String actualValuesReadAll = rpcActualResultReadAll.get("value").asText(); log.warn("ObserveReadAll: [{}]", actualValuesReadAll); - assertEquals(2, actualValuesReadAll.split(",").length); + assertEquals(3, actualValuesReadAll.split(",").length); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java index cb9702aa70..cb92aada30 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java @@ -50,22 +50,28 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest */ @Test public void testReadAllObjectsInClientById_Result_CONTENT_Value_IsLwM2mObject_IsInstances() throws Exception { - expectedObjectIdVers.forEach(expected -> { - try { - String actualResult = sendRPCById((String) expected); - String expectedObjectId = pathIdVerToObjectId((String) expected); - LwM2mPath expectedPath = new LwM2mPath(expectedObjectId); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources="; - if (expectedPath.getObjectId() == 2) { - expectedObjectInstances = "LwM2mObject [id=2, instances={}]"; + try { + expectedObjectIdVers.forEach(expected -> { + try { + String actualResult = sendRPCById((String) expected); + String expectedObjectId = pathIdVerToObjectId((String) expected); + LwM2mPath expectedPath = new LwM2mPath(expectedObjectId); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources="; + if (expectedPath.getObjectId() == 1) { + expectedObjectInstances = "LwM2mObject [id=1, instances={1="; + } else if (expectedPath.getObjectId() == 2) { + expectedObjectInstances = "LwM2mObject [id=2, instances={}]"; + } + assertTrue(rpcActualResult.get("value").asText().contains(expectedObjectInstances)); + } catch (Exception e) { + e.printStackTrace(); } - assertTrue(rpcActualResult.get("value").asText().contains(expectedObjectInstances)); - } catch (Exception e) { - e.printStackTrace(); - } - }); + }); + } catch (Exception e2){ + e2.printStackTrace(); + } } /** diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java index f9e0725084..6275a657b5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.rpc.sql; import com.fasterxml.jackson.databind.node.ObjectNode; import org.eclipse.leshan.core.ResponseCode; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; @@ -29,10 +30,11 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest { + @Ignore /** * WriteAttributes {"id":"/3/0/14","attributes":{"pmax":100, "pmin":10}} * if not implemented: - * {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"} + * {"result":"BAD_REQUEST","error":"Content Format is mandatory"} * if implemented: * {"result":"CHANGED"} */ @@ -42,8 +44,8 @@ public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MInte String expectedValue = "{\"pmax\":100, \"pmin\":10}"; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText()); - String expected = "not implemented"; + assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); + String expected = "Content Format is mandatory"; String actual = rpcActualResult.get("error").asText(); assertTrue(actual.equals(expected)); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java index 3aff2e4725..e1f1cf2f15 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java @@ -161,8 +161,8 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes int resourceInstanceId25 = 25; String expectedValue0 = "00ad45675600"; String expectedValue25 = "25ad45675600cdef"; - String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\", \"" + resourceInstanceId25 + "\":\"" + expectedValue25 + "\"}}"; - String actualResult = sendRPCWriteObjectById("WriteUpdate", expectedPath, expectedValue); + String expectedValueResourcesObject19 = "{\"" + RESOURCE_ID_0 + "\":{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\", \"" + resourceInstanceId25 + "\":\"" + expectedValue25 + "\"}}"; + String actualResult = sendRPCWriteObjectById("WriteUpdate", expectedPath, expectedValueResourcesObject19); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); String expectedPath0 = expectedPath + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId0; @@ -181,29 +181,37 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes /** * ResourceInstance + KeySingleResource + IdSingleResource - * WriteComposite {"nodes":{"/19/1/0/2":"00001234", "UtfOffset":"+04", "/3/0/15":"Kiyv/Europe"}} + * WriteComposite {"/19_1.1/0":{"0":{"0":"00ad45675600", "25":"25ad45675600cdef"}}, "UtfOffset":"+04", "/3_1.0/0/15":"Kiyv/Europe"} * {"result":"CHANGED"} */ @Test - public void testWriteCompositeValueSingleResourceResourceInstanceByIdKey_Result_CHANGED() throws Exception { - int resourceInstanceId2 = 2; - String expectedPath19_1_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId2; - String expectedValue19_1_0_2 = "00001234"; + public void testWriteCompositeValueSingleResourceWithMultiResourceInstanceByIdKey_Result_CHANGED() throws Exception { + String expectedPath_19_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0; + int resourceInstanceId0 = 0; + int resourceInstanceId25 = 25; + String expectedValue0 = "00ad45675600"; + String expectedValue25 = "25ad45675600cdef"; + String expectedValue_19_Resources = "{\"" + RESOURCE_ID_0 + "\":{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\", \"" + resourceInstanceId25 + "\":\"" + expectedValue25 + "\"}}"; + String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedValue3_0_14 = "+04"; String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15; String expectedValue3_0_15 = "Kiyv/Europe"; - String nodes = "{\"" + expectedPath19_1_0_2 + "\":\"" + expectedValue19_1_0_2 + "\", \"" + expectedKey3_0_14 + + String nodes = "{\"" + expectedPath_19_0 + "\":" + expectedValue_19_Resources + ", \"" + expectedKey3_0_14 + "\":\"" + expectedValue3_0_14 + "\", \"" + expectedPath3_0_15 + "\":\"" + expectedValue3_0_15 + "\"}"; String actualResult = sendCompositeRPC(nodes); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); - actualResult = sendRPCReadById(expectedPath19_1_0_2); + actualResult = sendRPCReadById(expectedPath_19_0 + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId0); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String actualValues = rpcActualResult.get("value").asText(); - String expected = "LwM2mResourceInstance [id=" + resourceInstanceId2 + ", value=" + expectedValue19_1_0_2.length()/2 + "Bytes, type=OPAQUE]"; + String expected = "LwM2mResourceInstance [id=" + resourceInstanceId0 + ", value=" + expectedValue0.length()/2 + "Bytes, type=OPAQUE]"; assertTrue(actualValues.contains(expected)); - actualResult = sendRPCReadByKey(expectedKey3_0_14); + actualResult = sendRPCReadById(expectedPath_19_0 + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId25); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + actualValues = rpcActualResult.get("value").asText(); + expected = "LwM2mResourceInstance [id=" + resourceInstanceId25 + ", value=" + expectedValue25.length()/2 + "Bytes, type=OPAQUE]"; + assertTrue(actualValues.contains(expected)); actualResult = sendRPCReadByKey(expectedKey3_0_14); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); actualValues = rpcActualResult.get("value").asText(); expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue3_0_14 + ", type=STRING]"; @@ -217,29 +225,36 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes /** * multipleResource == error - * bad - cannot be used for value Json, only primitive: SingleResource, ResourceInstance (for Json: WriteUpdate, WriteReplace) - * WriteComposite {"nodes":{"/19/0/0":{"0":"abcd5678", "10":"abcd5678"}}} + * WriteComposite {"/19_1.1/0/0":{"0":"00ad45675600", "25":"25ad45675600cdef"}} + * {"result":"CHANGED"} */ @Test - public void testWriteCompositeValueSingleMultipleResourceByIdKey_Result_BAD_REQUEST_WriteComposite_operation_for_SingleResources_or_and_ResourceInstance() throws Exception { - String nodes = "{\"/19/0/0\":{\"0\":\"abcd5678\", \"10\":\"abcd5678\"}}"; + public void testWriteCompositeValueSingleMultipleResourceOpaqueValueInputHexStringByIdKey_Result_CHANGED() throws Exception { + String expectedPath_19_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0; + int resourceInstanceId0 = 0; + int resourceInstanceId25 = 25; + String expectedValue0 = "00ad45675600"; + String expectedValue25 = "25ad45675600cdef"; + String nodes = "{\"" + expectedPath_19_0 + "/" + RESOURCE_ID_0 + "\":{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\", \"" + resourceInstanceId25 + "\":\"" + expectedValue25 + "\"}}"; + String actualResult = sendCompositeRPC(nodes); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); - String actualValues = rpcActualResult.get("error").asText(); - String expectedNodes = nodes.replaceAll("\"", "").replaceAll(":", "="); - String expected = String.format("nodes: %s is not validate value. " + - "The WriteComposite operation is only used for SingleResources or/and ResourceInstance.", expectedNodes); - assertEquals(expected, actualValues); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + actualResult = sendRPCReadById(expectedPath_19_0 + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId0); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + String actualValues = rpcActualResult.get("value").asText(); + String expected = "LwM2mResourceInstance [id=" + resourceInstanceId0 + ", value=" + expectedValue0.length()/2 + "Bytes, type=OPAQUE]"; + assertTrue(actualValues.contains(expected)); + actualResult = sendRPCReadById(expectedPath_19_0 + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId25); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + actualValues = rpcActualResult.get("value").asText(); + expected = "LwM2mResourceInstance [id=" + resourceInstanceId25 + ", value=" + expectedValue25.length()/2 + "Bytes, type=OPAQUE]"; + assertTrue(actualValues.contains(expected)); } /** * update_resourceInstances&update_singleResource - * new ResourceInstance if Resource is Multiple & Resource Single - * - WriteReplace {"id":"/19_1.2/1/0","value":{"2":ddff12"}} - * - WriteReplace {"key":"UtfOffset","value":"+04"} - * - WriteReplace {"id":"/3/0/15","value":"Kiyv/Europe"} * WriteComposite {"nodes":{"/19_1.1/1/0/2":"00001234", "UtfOffset":"+04", "/3/0/15":"Kiyv/Europe"}}} * {"result":"CHANGED"} */ @@ -331,4 +346,4 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes String setRpcRequest = "{\"method\": \"WriteComposite\", \"params\": {\"nodes\":" + nodes + "}}"; return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); } -} \ No newline at end of file +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index 69b257d910..b97adf035c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -63,7 +63,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import static org.awaitility.Awaitility.await; -import static org.eclipse.leshan.client.object.Security.noSecBootstap; +import static org.eclipse.leshan.client.object.Security.noSecBootstrap; import static org.junit.Assert.assertEquals; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_DEREGISTRATION_STARTED; @@ -89,6 +89,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M protected static final String SERVER_STORE_PWD = "server_ks_password"; protected static final String SERVER_CERT_ALIAS = "server"; protected static final String SERVER_CERT_ALIAS_BS = "bootstrap"; + protected static final Security SECURITY_NO_SEC_BS = noSecBootstrap(URI_BS);; protected final X509Certificate serverX509Cert; // server certificate signed by rootCA protected final X509Certificate serverX509CertBs; // serverBs certificate signed by rootCA protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK @@ -171,7 +172,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M LwM2MClientState finishState) throws Exception { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(type)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - this.basicTestConnection(noSecBootstap(URI_BS), + this.basicTestConnection(null , SECURITY_NO_SEC_BS, deviceCredentials, COAP_CONFIG_BS, clientEndpoint, @@ -183,22 +184,24 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M false); } - protected void basicTestConnection(Security security, + protected void basicTestConnection(Security security, Security securityBs, LwM2MDeviceCredentials deviceCredentials, Configuration coapConfig, String endpoint, Lwm2mDeviceProfileTransportConfiguration transportConfiguration, String awaitAlias, Set expectedStatuses, - boolean isBootstrap, + boolean isAwaitObserveReadAll, LwM2MClientState finishState, boolean isStartLw) throws Exception { createDeviceProfile(transportConfiguration); final Device device = createDevice(deviceCredentials, endpoint); device.getId().getId().toString(); - createNewClient(security, coapConfig, true, endpoint, isBootstrap, null); + createNewClient(security, securityBs, coapConfig, true, endpoint); lwM2MTestClient.start(isStartLw); - awaitObserveReadAll(0, isBootstrap, device.getId().getId().toString()); + if (isAwaitObserveReadAll) { + awaitObserveReadAll(0, device.getId().getId().toString()); + } await(awaitAlias) .atMost(40, TimeUnit.SECONDS) .until(() -> { @@ -220,34 +223,31 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); this.basicTestConnectionBootstrapRequestTrigger( SECURITY_NO_SEC, + SECURITY_NO_SEC_BS, deviceCredentials, COAP_CONFIG, clientEndpoint, transportConfiguration, awaitAlias, expectedStatusesRegistrationLwm2mSuccess, - expectedStatusesRegistrationBsSuccess, - false, - SECURITY_NO_SEC_BS); + expectedStatusesRegistrationBsSuccess); } - private void basicTestConnectionBootstrapRequestTrigger(Security security, + private void basicTestConnectionBootstrapRequestTrigger(Security security, Security securityBs, LwM2MDeviceCredentials deviceCredentials, Configuration coapConfig, String endpoint, Lwm2mDeviceProfileTransportConfiguration transportConfiguration, String awaitAlias, Set expectedStatusesLwm2m, - Set expectedStatusesBs, - boolean isBootstrap, - Security securityBs) throws Exception { + Set expectedStatusesBs) throws Exception { createDeviceProfile(transportConfiguration); final Device device = createDevice(deviceCredentials, endpoint); String deviceIdStr = device.getId().getId().toString(); - createNewClient(security, coapConfig, true, endpoint, isBootstrap, securityBs); + createNewClient(security, securityBs, coapConfig, true, endpoint); lwM2MTestClient.start(true); - awaitObserveReadAll(0, isBootstrap, deviceIdStr); + awaitObserveReadAll(0, deviceIdStr); await(awaitAlias) .atMost(40, TimeUnit.SECONDS) .until(() -> { @@ -263,7 +263,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesLwm2m)); String executedPath = "/" + OBJECT_ID_1 + "_" + lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_1).version - + "/0/" + RESOURCE_ID_9; + + "/" +serverId + "/" + RESOURCE_ID_9; lwM2MTestClient.setClientStates(new HashSet<>()); String actualResult = sendRPCSecurityExecuteById(executedPath, deviceIdStr, endpoint); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -337,7 +337,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M default: throw new IllegalStateException("Unexpected value: " + mode); } - bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs : shortServerId); + bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs0 : shortServerId); bootstrapServerCredential.setBootstrapServerIs(isBootstrap); bootstrapServerCredential.setHost(isBootstrap ? hostBs : host); bootstrapServerCredential.setPort(isBootstrap ? securityPortBs : securityPort); @@ -443,3 +443,4 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); } } + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index 49c28cc13c..33e2965d62 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -19,7 +19,6 @@ import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_SUCCESS; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH; @@ -39,7 +38,7 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT // Bootstrap + Lwm2m @Test public void testWithNoSecConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS; + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + BOTH.name(); String awaitAlias = "await on client state (NoSecBS two section)"; basicTestConnectionBefore(clientEndpoint, awaitAlias, BOTH, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS); } @@ -51,20 +50,7 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT basicTestConnectionBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS); } - @Test - public void testWithNoSecConnectBsSuccess_UpdateBootstrapSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + BOOTSTRAP_ONLY.name(); - String awaitAlias = "await on client state (NoSecBS Bootstrap section)"; - basicTestConnectionBefore(clientEndpoint, awaitAlias, BOOTSTRAP_ONLY, expectedStatusesBsSuccess, ON_BOOTSTRAP_SUCCESS); - } - - @Test - public void testWithNoSecConnectBsSuccess_UpdateNoneSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + NONE.name(); - String awaitAlias = "await on client state (NoSecBS None section)"; - basicTestConnectionBefore(clientEndpoint, awaitAlias, NONE, expectedStatusesBsSuccess, ON_BOOTSTRAP_SUCCESS); - } - + // Bs trigger @Test public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateTwoSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOTH.name(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index f041f0be55..a6211b2594 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -55,13 +55,14 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); this.basicTestConnection(security, + null, deviceCredentials, COAP_CONFIG, clientEndpoint, transportConfiguration, "await on client state (Psk_Lwm2m)", expectedStatusesRegistrationLwm2mSuccess, - false, + true, ON_REGISTRATION_SUCCESS, true); } @@ -100,15 +101,15 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes Hex.decodeHex(keyPsk.toCharArray())); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, BOTH)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); - this.basicTestConnection(securityBs, + this.basicTestConnection(null, securityBs, deviceCredentials, COAP_CONFIG_BS, clientEndpoint, transportConfiguration, "await on client state (PskBS two section)", expectedStatusesRegistrationBsSuccess, - true, + false, ON_REGISTRATION_SUCCESS, - true); + false); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index c75ecbd820..73176b1646 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -49,21 +49,21 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes RPKClientCredential clientCredentials = new RPKClientCredential(); clientCredentials.setEndpoint(clientEndpoint); clientCredentials.setKey(Base64.encodeBase64String(certificate.getPublicKey().getEncoded())); - Security securityBs = rpk(SECURE_URI, + Security security = rpk(SECURE_URI, shortServerId, certificate.getPublicKey().getEncoded(), privateKey.getEncoded(), serverX509Cert.getPublicKey().getEncoded()); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(RPK, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, RPK, false); - this.basicTestConnection(securityBs, + this.basicTestConnection(security, null, deviceCredentials, COAP_CONFIG, clientEndpoint, transportConfiguration, "await on client state (Rpk_Lwm2m)", expectedStatusesRegistrationLwm2mSuccess, - false, + true, ON_REGISTRATION_SUCCESS, true); } @@ -117,14 +117,14 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes serverX509CertBs.getPublicKey().getEncoded()); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(RPK, BOTH)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, clientPrivateKeyFromCertTrust, certificate, RPK, false); - this.basicTestConnection(securityBs, + this.basicTestConnection(null, securityBs, deviceCredentials, COAP_CONFIG_BS, clientEndpoint, transportConfiguration, "await on client state (RpkBS two section)", expectedStatusesRegistrationBsSuccess, - true, + false, ON_REGISTRATION_SUCCESS, true); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java index 1a908bffb8..5970f16958 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java @@ -57,13 +57,14 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); this.basicTestConnection(security, + null, deviceCredentials, COAP_CONFIG, clientEndpoint, transportConfiguration, "await on client state (X509_Trust_Lwm2m)", expectedStatusesRegistrationLwm2mSuccess, - false, + true, ON_REGISTRATION_SUCCESS, true); } @@ -118,13 +119,14 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); this.basicTestConnection(security, + null, deviceCredentials, COAP_CONFIG_BS, clientEndpoint, transportConfiguration, "await on client state (X509NoTrust two section)", expectedStatusesRegistrationBsSuccess, - true, + false, ON_REGISTRATION_SUCCESS, true); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java index 98a5d018bc..d07d0c1df6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java @@ -51,13 +51,14 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); this.basicTestConnection(security, + null, deviceCredentials, COAP_CONFIG, clientEndpoint, transportConfiguration, "await on client state (X509_Trust_Lwm2m)", expectedStatusesRegistrationLwm2mSuccess, - false, + true, ON_REGISTRATION_SUCCESS, true); } @@ -78,13 +79,14 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); this.basicTestConnection(security, + null, deviceCredentials, COAP_CONFIG_BS, clientEndpoint, transportConfiguration, "await on client state (X509Trust two section)", expectedStatusesRegistrationBsSuccess, - true, + false, ON_REGISTRATION_SUCCESS, true); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java index b59c5fadea..8644ff1b79 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java @@ -26,7 +26,7 @@ public class LwM2MServerSecurityConfig implements Serializable { @Schema(description = "Server short Id. Used as link to associate server Object Instance. This identifier uniquely identifies each LwM2M Server configured for the LwM2M Client. " + "This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'. " + - "The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.", example = "123", accessMode = Schema.AccessMode.READ_ONLY) + "The values ID:1 and ID:65534 values MUST NOT be used for identifying the LwM2M Server.", example = "123", accessMode = Schema.AccessMode.READ_ONLY) protected Integer shortServerId = 123; /** Security -> ObjectId = 0 'LWM2M Security' */ @Schema(description = "Is Bootstrap Server or Lwm2m Server. " + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDDFFileParser.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDDFFileParser.java index 7c1b9307c5..1a84f45b7d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDDFFileParser.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDDFFileParser.java @@ -17,8 +17,6 @@ package org.thingsboard.server.common.data.util; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.LwM2m; -import org.eclipse.leshan.core.model.DDFFileValidator; -import org.eclipse.leshan.core.model.DefaultDDFFileValidator; import org.eclipse.leshan.core.model.InvalidDDFFileException; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; @@ -41,7 +39,7 @@ import java.util.Map; @Slf4j public class TbDDFFileParser { - private static final DDFFileValidator ddfFileValidator = new DefaultDDFFileValidator(); + private static final TbDefaultDDFFileValidator ddfFileValidator = new TbDefaultDDFFileValidator(); public List parse(InputStream inputStream, String streamName) throws InvalidDDFFileException, IOException { @@ -269,4 +267,4 @@ public class TbDDFFileParser { } return new ResourceModel(id, name, operations, multiple, mandatory, type, rangeEnumeration, units, description); } -} \ No newline at end of file +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDefaultDDFFileValidator.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDefaultDDFFileValidator.java new file mode 100644 index 0000000000..36ed0f668d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDefaultDDFFileValidator.java @@ -0,0 +1,129 @@ +/** + * 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.common.data.util; + + +import org.eclipse.leshan.core.LwM2m.LwM2mVersion; +import org.eclipse.leshan.core.model.DDFFileValidator; +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.eclipse.leshan.core.util.Validate; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; + +import javax.xml.XMLConstants; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; +import java.io.IOException; +import java.io.InputStream; + +/** + * A DDF File Validator. + *

+ * Validate a DDF File against the embedded LWM2M schema. + *

+ * Support LWM2M version 1.0 and 1.1. + */ + +public class TbDefaultDDFFileValidator implements DDFFileValidator { + private static String LWM2M_V1_0_SCHEMA_PATH = "/schemas/LWM2M.xsd"; + private static String LWM2M_V1_1_SCHEMA_PATH = "/schemas/LWM2M-v1_1.xsd"; + + private final String schema; + + /** + * Create a {@link DDFFileValidator} using the LWM2M v1.1 schema. + */ + public TbDefaultDDFFileValidator() { + this(LwM2mVersion.V1_1); + } + + /** + * Create a {@link DDFFileValidator} using schema corresponding to LWM2M {@link LwM2mVersion}. + */ + public TbDefaultDDFFileValidator(LwM2mVersion version) { + Validate.notNull(version, "version must not be null"); + if (LwM2mVersion.V1_0.equals(version)) { + schema = LWM2M_V1_0_SCHEMA_PATH; + } else if (LwM2mVersion.V1_1.equals(version)) { + schema = LWM2M_V1_1_SCHEMA_PATH; + } else { + throw new IllegalStateException(String.format("Unsupported version %s", version)); + } + } + + @Override + public void validate(Node xmlToValidate) throws InvalidDDFFileException { + try { + validate(new DOMSource(xmlToValidate)); + } catch (SAXException | IOException e) { + throw new InvalidDDFFileException(e); + } + } + + /** + * Validate a XML {@link Source} against the embedded LWM2M Schema. + * + * @param xmlToValidate an XML source to validate + * @throws SAXException see {@link Validator#validate(Source)} + * @throws IOException see {@link Validator#validate(Source)} + */ + public void validate(Source xmlToValidate) throws SAXException, IOException { + Validator validator = getEmbeddedLwM2mSchema().newValidator(); + validator.validate(xmlToValidate); + } + + /** + * Get the Embedded the LWM2M.xsd Schema. + * + * @throws SAXException see {@link SchemaFactory#newSchema(Source)} + */ + protected Schema getEmbeddedLwM2mSchema() throws SAXException { + InputStream inputStream = DDFFileValidator.class.getResourceAsStream(schema); + Source source = new StreamSource(inputStream); + SchemaFactory schemaFactory = createSchemaFactory(); + return schemaFactory.newSchema(source); + } + + protected SchemaFactory createSchemaFactory() { + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); +// SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI); + try { + // Create Safe SchemaFactory (not vulnerable to XXE Attacks) + // -------------------------------------------------------- + // There is several recommendation from different source we try to apply all, even if some are maybe + // redundant. + + // from : + // https://semgrep.dev/docs/cheat-sheets/java-xxe/ + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + + // from : + // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#schemafactory +// factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); +// factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + + } catch (SAXNotRecognizedException | SAXNotSupportedException e) { + throw new IllegalStateException("Unable to create SchemaFactory", e); + } + return factory; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index 75a2f936ca..96a02bdf97 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -15,13 +15,20 @@ */ package org.thingsboard.server.transport.lwm2m.bootstrap; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.elements.config.Configuration; import org.eclipse.californium.scandium.config.DtlsConfig; -import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.leshan.core.endpoint.Protocol; import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager; -import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer; -import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder; +import org.eclipse.leshan.server.bootstrap.LeshanBootstrapServer; +import org.eclipse.leshan.server.bootstrap.LeshanBootstrapServerBuilder; +import org.eclipse.leshan.server.californium.bootstrap.LwM2mBootstrapPskStore; +import org.eclipse.leshan.server.californium.bootstrap.endpoint.CaliforniumBootstrapServerEndpointsProvider; +import org.eclipse.leshan.server.californium.bootstrap.endpoint.coap.CoapBootstrapServerProtocolProvider; +import org.eclipse.leshan.server.californium.bootstrap.endpoint.coaps.CoapsBootstrapServerProtocolProvider; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.config.ssl.SslCredentials; @@ -33,16 +40,12 @@ import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MInMemoryBoots import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; +import java.net.InetSocketAddress; import java.security.cert.X509Certificate; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CURVES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT; -import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_ROLE; -import static org.eclipse.californium.scandium.config.DtlsConfig.DtlsRole.SERVER_ONLY; import static org.thingsboard.server.transport.lwm2m.server.DefaultLwM2mTransportService.PSK_CIPHER_SUITES; import static org.thingsboard.server.transport.lwm2m.server.DefaultLwM2mTransportService.RPK_OR_X509_CIPHER_SUITES; import static org.thingsboard.server.transport.lwm2m.server.LwM2MNetworkConfig.getCoapConfig; @@ -82,24 +85,121 @@ public class LwM2MTransportBootstrapService { public LeshanBootstrapServer getLhBootstrapServer() { LeshanBootstrapServerBuilder builder = new LeshanBootstrapServerBuilder(); - builder.setLocalAddress(bootstrapConfig.getHost(), bootstrapConfig.getPort()); - builder.setLocalSecureAddress(bootstrapConfig.getSecureHost(), bootstrapConfig.getSecurePort()); - /* Create CoAP Config */ - builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig)); + // Create Californium Endpoints Provider: + // ------------------ + // Create Server Endpoints Provider + CaliforniumBootstrapServerEndpointsProvider.Builder endpointsBuilder = new CaliforniumBootstrapServerEndpointsProvider.Builder( + // Add coap Protocol support + new CoapBootstrapServerProtocolProvider(), + + // Add coaps/dtls protocol support + new CoapsBootstrapServerProtocolProvider(c -> { + if (this.bootstrapConfig.getSslCredentials() != null) { + c.setAdvancedCertificateVerifier(certificateVerifier); + c.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, RPK_OR_X509_CIPHER_SUITES); + } else { + log.info("Unable to load X509 files for LWM2MServer"); + LwM2mBootstrapPskStore lwM2mBsPskStore = new LwM2mBootstrapPskStore(lwM2MBootstrapSecurityStore); + c.setAdvancedPskStore(lwM2mBsPskStore); + c.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, PSK_CIPHER_SUITES); + } + })); + + + // Create Californium Configuration + Configuration serverCoapConfig = endpointsBuilder.createDefaultConfiguration(); + getCoapConfig(serverCoapConfig, bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(),serverConfig); + serverCoapConfig.setTransient(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY); + serverCoapConfig.set(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, serverConfig.isRecommendedCiphers()); + serverCoapConfig.setTransient(DtlsConfig.DTLS_CONNECTION_ID_LENGTH); + int cid = 6; + if (cid > 0) { + serverCoapConfig.set(DtlsConfig.DTLS_CONNECTION_ID_LENGTH, cid); + } + + serverCoapConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, serverConfig.isRecommendedSupportedGroups()); + + serverCoapConfig.setTransient(DTLS_RETRANSMISSION_TIMEOUT); + serverCoapConfig.set(DTLS_RETRANSMISSION_TIMEOUT, serverConfig.getDtlsRetransmissionTimeout(), MILLISECONDS); + + + + /* Create DTLS Config */ + this.setServerWithCredentials(builder); +// DtlsConnectorConfig dtlsConfig; +// try { +// dtlsConfig = dtlsConfigBuilder.build(); +// } catch (IllegalStateException e) { +// log.warn("Unable to create DTLS config for endpont {}.", endpointUri.toString(), e); +// return null; +// } +// +// Connector dTLSConnector = new DTLSConnector(dtlsConfig); - /* Create and Set DTLS Config */ - DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig)); +// endpointsBuilder.setConnector(dTLSConnector); +// endpointsBuilder.setConfiguration(serverCoapConfig); - dtlsConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, serverConfig.isRecommendedSupportedGroups()); - dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, serverConfig.isRecommendedCiphers()); - dtlsConfig.set(DTLS_RETRANSMISSION_TIMEOUT, serverConfig.getDtlsRetransmissionTimeout(), MILLISECONDS); - dtlsConfig.set(DTLS_ROLE, SERVER_ONLY); - setServerWithCredentials(builder, dtlsConfig); +// endpointsBuilder.setLoggingTag(String.format("[%s]", "/" + "options.getUriPathString()")); +// endpointsBuilder.setEndpointContextMatcher(new Lwm2mEndpointContextMatcher()); - /* Set DTLS Config */ - builder.setDtlsConfig(dtlsConfig); + /* Create credentials */ + + + + + // Set Californium Configuration + endpointsBuilder.setConfiguration(serverCoapConfig); + + + // Create CoAP endpoint + InetSocketAddress coapAddr = new InetSocketAddress(bootstrapConfig.getHost(), bootstrapConfig.getPort()); + endpointsBuilder.addEndpoint(coapAddr, Protocol.COAP); + + // Create CoAP over DTLS endpoint + InetSocketAddress coapsAddr = new InetSocketAddress(bootstrapConfig.getSecureHost(), bootstrapConfig.getSecurePort()); + endpointsBuilder.addEndpoint(coapsAddr, Protocol.COAPS); + + +// builder.setLocalAddress(config.getHost(), config.getPort()); +// builder.setLocalSecureAddress(config.getSecureHost(), config.getSecurePort()); +// builder.setDecoder(new DefaultLwM2mDecoder()); + /* Use a magic converter to support bad type send by the UI. */ +// builder.setEncoder(new DefaultLwM2mEncoder(LwM2mValueConverterImpl.getInstance())); + + /* Create CoAP Config */ +// builder.setCoapConfig(getCoapConfig(config.getPort(), config.getSecurePort(), config)); + +// +// /* Set securityStore with new registrationStore */ +// builder.setSecurityStore(securityStore); +// builder.setRegistrationStore(registrationStore); +// +// +// // Create LWM2M server +// builder.setEndpointsProviders(endpointsBuilder.build()); +// +// +// +// builder.setLocalAddress(bootstrapConfig.getHost(), bootstrapConfig.getPort()); +// builder.setLocalSecureAddress(bootstrapConfig.getSecureHost(), bootstrapConfig.getSecurePort()); +// +// /* Create CoAP Config */ +// builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig)); +// +// +// /* Create and Set DTLS Config */ +// DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig)); +// +// dtlsConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, serverConfig.isRecommendedSupportedGroups()); +// dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, serverConfig.isRecommendedCiphers()); +// dtlsConfig.set(DTLS_RETRANSMISSION_TIMEOUT, serverConfig.getDtlsRetransmissionTimeout(), MILLISECONDS); +// dtlsConfig.set(DTLS_ROLE, SERVER_ONLY); +// setServerWithCredentials(builder, dtlsConfig); +// +// /* Set DTLS Config */ +// builder.setDtlsConfig(dtlsConfig); /* Set securityStore with new ConfigStore */ builder.setConfigStore(lwM2MInMemoryBootstrapConfigStore); @@ -112,22 +212,19 @@ public class LwM2MTransportBootstrapService { builder.setSessionManager(sessionManager); /* Create BootstrapServer */ + builder.setEndpointsProviders(endpointsBuilder.build()); return builder.build(); } - private void setServerWithCredentials(LeshanBootstrapServerBuilder builder, DtlsConnectorConfig.Builder dtlsConfig) { + private void setServerWithCredentials(LeshanBootstrapServerBuilder builder) { if (this.bootstrapConfig.getSslCredentials() != null) { SslCredentials sslCredentials = this.bootstrapConfig.getSslCredentials(); builder.setPublicKey(sslCredentials.getPublicKey()); builder.setPrivateKey(sslCredentials.getPrivateKey()); builder.setCertificateChain(sslCredentials.getCertificateChain()); - dtlsConfig.setAdvancedCertificateVerifier(certificateVerifier); - dtlsConfig.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, RPK_OR_X509_CIPHER_SUITES); } else { /* by default trust all */ builder.setTrustedCertificates(new X509Certificate[0]); - log.info("Unable to load X509 files for BootStrap Server"); - dtlsConfig.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, PSK_CIPHER_SUITES); } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java index ea3b8e60c9..03b99f12c9 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java @@ -16,10 +16,13 @@ package org.thingsboard.server.transport.lwm2m.bootstrap.secure; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.peer.IpPeer; +import org.eclipse.leshan.core.peer.LwM2mPeer; +import org.eclipse.leshan.core.peer.PskIdentity; +import org.eclipse.leshan.core.peer.X509Identity; import org.eclipse.leshan.core.request.BootstrapDownlinkRequest; import org.eclipse.leshan.core.request.BootstrapFinishRequest; import org.eclipse.leshan.core.request.BootstrapRequest; -import org.eclipse.leshan.core.request.Identity; import org.eclipse.leshan.core.response.LwM2mResponse; import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore; import org.eclipse.leshan.server.bootstrap.BootstrapFailureCause; @@ -39,6 +42,7 @@ import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapSecu import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapTaskProvider; import org.thingsboard.server.transport.lwm2m.server.client.LwM2MAuthException; +import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -63,7 +67,7 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession * @param bsSecurityStore the {@link BootstrapSecurityStore} used by default {@link SecurityChecker}. */ public LwM2mDefaultBootstrapSessionManager(BootstrapSecurityStore bsSecurityStore, BootstrapConfigStore configStore, TransportService transportService) { - this(bsSecurityStore, new SecurityChecker(), new LwM2MBootstrapConfigStoreTaskProvider(configStore), + this(bsSecurityStore, configStore, new SecurityChecker(), new LwM2MBootstrapConfigStoreTaskProvider(configStore), new StandardBootstrapModelProvider()); this.transportService = transportService; } @@ -74,9 +78,9 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession * @param bsSecurityStore the {@link BootstrapSecurityStore} used by {@link SecurityChecker}. * @param securityChecker used to accept or refuse new {@link BootstrapSession}. */ - public LwM2mDefaultBootstrapSessionManager(BootstrapSecurityStore bsSecurityStore, SecurityChecker securityChecker, + public LwM2mDefaultBootstrapSessionManager(BootstrapSecurityStore bsSecurityStore, BootstrapConfigStore configStore, SecurityChecker securityChecker, LwM2MBootstrapTaskProvider tasksProvider, LwM2mBootstrapModelProvider modelProvider) { - super(bsSecurityStore, securityChecker, tasksProvider, modelProvider); + super(bsSecurityStore, configStore); this.bsSecurityStore = bsSecurityStore; this.securityChecker = securityChecker; this.tasksProvider = tasksProvider; @@ -84,23 +88,24 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession } @Override - public BootstrapSession begin(BootstrapRequest request, Identity clientIdentity) { - boolean authorized = true; + public BootstrapSession begin(BootstrapRequest request, LwM2mPeer sender, URI endpointUsed) { + boolean authorized = sender.getIdentity().isSecure(); Iterator securityInfos = null; - try { + try { if (bsSecurityStore != null && securityChecker != null) { - if (clientIdentity.isPSK()) { - SecurityInfo securityInfo = bsSecurityStore.getByIdentity(clientIdentity.getPskIdentity()); + if (((IpPeer) sender).isPSK()) { + SecurityInfo securityInfo = bsSecurityStore.getByIdentity(((PskIdentity) sender.getIdentity()).getPskIdentity()); securityInfos = Collections.singletonList(securityInfo).iterator(); - } else if (!clientIdentity.isX509()) { + } + else if (!((IpPeer) sender).isX509()) { securityInfos = bsSecurityStore.getAllByEndpoint(request.getEndpointName()); } - authorized = this.checkSecurityInfo(request.getEndpointName(), clientIdentity, securityInfos); + authorized = this.checkSecurityInfo(request.getEndpointName(), sender, securityInfos); } } catch (LwM2MAuthException e) { authorized = false; } - DefaultBootstrapSession session = new DefaultBootstrapSession(request, clientIdentity, authorized); + DefaultBootstrapSession session = new DefaultBootstrapSession(request, sender, authorized, null, endpointUsed); if (authorized) { try { this.tasksProvider.put(session.getEndpoint()); @@ -239,9 +244,9 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession transportService.log(((LwM2MBootstrapSecurityStore) bsSecurityStore).getSessionByEndpoint(endpointName), logMsg); } - private boolean checkSecurityInfo(String endpoint, Identity clientIdentity, Iterator securityInfos) { - if (clientIdentity.isX509()) { - return clientIdentity.getX509CommonName().equals(endpoint) + private boolean checkSecurityInfo(String endpoint, LwM2mPeer clientIdentity, Iterator securityInfos) { + if (((IpPeer) clientIdentity).isX509()) { + return ((X509Identity)clientIdentity.getIdentity()).getX509CommonName().equals(endpoint) & ((LwM2MBootstrapSecurityStore) bsSecurityStore).getBootstrapConfigByEndpoint(endpoint) != null; } else { return securityChecker.checkSecurityInfos(endpoint, clientIdentity, securityInfos); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java index 53d72521b0..018648100d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java @@ -76,7 +76,8 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask @Override public Tasks getTasks(BootstrapSession session, List previousResponse) { - BootstrapConfig config = store.get(session.getEndpoint(), session.getIdentity(), session); +// BootstrapConfig config = store.get(session.getEndpoint(), session.getClientTransportData().getIdentity(), session); + BootstrapConfig config = store.get(session); if (config == null) { return null; } @@ -150,7 +151,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask * The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server. * "Short Server ID": * - Link Instance (lwm2m Server) hase linkParams with key = "ssid" value = "shortId" (ver lvm2m = 1.1). - * - Link Instance (bootstrap Server) hase not linkParams with key = "ssid" (ver lvm2m = 1.1). + * - Link Instance (bootstrap Server) hase not linkParams with key = "ssid" (ver lvm2m = 1.0). */ protected void findSecurityInstanceId(Link[] objectLinks, String endpoint) { log.info("Object after discover: [{}]", objectLinks); @@ -159,14 +160,14 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask try { LwM2mPath path = new LwM2mPath(link.getUriReference()); if (path.isObjectInstance()) { - if (link.getLinkParams().containsKey("ssid")) { - int serverId = Integer.parseInt(link.getLinkParams().get("ssid").getUnquoted()); + if (link.getAttributes().get("ssid") != null) { + int serverId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue()); if (!lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(serverId)) { lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId()); } else { log.error("Invalid lwm2mSecurityInstance by [{}]", path.getObjectInstanceId()); } - lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(Integer.valueOf(link.getLinkParams().get("ssid").getUnquoted()), path.getObjectInstanceId()); + lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId()); } else { if (!this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(0)) { this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(BOOTSTRAP_DEFAULT_SHORT_ID, path.getObjectInstanceId()); @@ -221,7 +222,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask LwM2mPath path = new LwM2mPath(link.getUriReference()); if (!path.isRoot() && path.getObjectId() < 3) { if (path.isObject()) { - String ver = link.getLinkParams().get("ver") != null ? link.getLinkParams().get("ver").getUnquoted() : "1.0"; + String ver = link.getAttributes().get("ver") != null ? link.getAttributes().get("ver").getCoreLinkValue() : "1.0"; this.supportedObjects.put(path.getObjectId(), ver); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java index 5dd4e41c3e..be4d0b15ad 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapSecurityStore.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.bootstrap.store; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.peer.OscoreIdentity; import org.eclipse.leshan.server.bootstrap.BootstrapConfig; import org.eclipse.leshan.server.bootstrap.EditableBootstrapConfigStore; import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException; @@ -98,6 +99,11 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { } } + @Override + public SecurityInfo getByOscoreIdentity(OscoreIdentity oscoreIdentity) { + return null; + } + public TbLwM2MSecurityInfo getX509ByEndpoint(String endPoint) { TbLwM2MSecurityInfo store = lwM2MCredentialsSecurityInfoValidator.getEndpointSecurityInfoByCredentialsId(endPoint, BOOTSTRAP); this.addValueToStore(store, store.getEndpoint()); @@ -210,4 +216,4 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { } return securityInfo; } -} \ No newline at end of file +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java index 3d3a96ac9e..1466012e2b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java @@ -58,17 +58,25 @@ public class LwM2MConfigurationChecker extends ConfigurationChecker { for (Map.Entry e : config.servers.entrySet()) { BootstrapConfig.ServerConfig srvCfg = e.getValue(); - // shortId checks - if (srvCfg.shortId == 0) { - throw new InvalidConfigurationException("short ID must not be 0"); - } - // look for security entry BootstrapConfig.ServerSecurity security = getSecurityEntry(config, srvCfg.shortId); - if (security == null) { throw new InvalidConfigurationException("no security entry for server instance: " + e.getKey()); } + // BS Server + if (security.bootstrapServer && srvCfg.shortId != 0) { + throw new InvalidConfigurationException("short ID must be 0"); + } + + // LwM2M Server + /** + * This identifier uniquely identifies each LwM2M Server configured for the LwM2M Client. + * This Resource MUST be set when the Bootstrap-Server Resource has false value. + * Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server (Section 6.3 of the LwM2M version 1.0 specification). + */ + if (!security.bootstrapServer && (srvCfg.shortId < 1 && srvCfg.shortId > 65534 )) { + throw new InvalidConfigurationException("Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server"); + } } } @@ -81,4 +89,4 @@ public class LwM2MConfigurationChecker extends ConfigurationChecker { return null; } -} \ No newline at end of file +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MInMemoryBootstrapConfigStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MInMemoryBootstrapConfigStore.java index bf2cc003a0..62339211bd 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MInMemoryBootstrapConfigStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MInMemoryBootstrapConfigStore.java @@ -16,9 +16,7 @@ package org.thingsboard.server.transport.lwm2m.bootstrap.store; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.request.Identity; import org.eclipse.leshan.server.bootstrap.BootstrapConfig; -import org.eclipse.leshan.server.bootstrap.BootstrapSession; import org.eclipse.leshan.server.bootstrap.ConfigurationChecker; import org.eclipse.leshan.server.bootstrap.InMemoryBootstrapConfigStore; import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException; @@ -39,8 +37,8 @@ public class LwM2MInMemoryBootstrapConfigStore extends InMemoryBootstrapConfigSt private final Lock writeLock = readWriteLock.writeLock(); protected final ConfigurationChecker configChecker = new LwM2MConfigurationChecker(); - @Override - public BootstrapConfig get(String endpoint, Identity deviceIdentity, BootstrapSession session) { + + public BootstrapConfig get(String endpoint) { return bootstrapByEndpoint.get(endpoint); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java index 63b2a60e8e..4a3eccd664 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java @@ -67,14 +67,14 @@ public class LwM2mCredentialsSecurityInfoValidator { new TransportServiceCallback<>() { @Override public void onSuccess(ValidateDeviceCredentialsResponse msg) { - log.trace("Validated credentials: [{}] [{}]", credentialsId, msg); + log.info("Validated credentials: [{}] [{}]", credentialsId, msg); resultSecurityStore[0] = createSecurityInfo(credentialsId, msg, keyValue); latch.countDown(); } @Override public void onError(Throwable e) { - log.trace("[{}] [{}] Failed to process credentials ", credentialsId, e); + log.info("[{}] [{}] Failed to process credentials ", credentialsId, e); TbLwM2MSecurityInfo result = new TbLwM2MSecurityInfo(); result.setEndpoint(credentialsId); resultSecurityStore[0] = result; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java index 7cdbe609e1..573e2fa1e7 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java @@ -18,9 +18,11 @@ package org.thingsboard.server.transport.lwm2m.secure; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.SecurityMode; -import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.peer.LwM2mPeer; +import org.eclipse.leshan.core.peer.X509Identity; import org.eclipse.leshan.core.request.UplinkRequest; import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.security.Authorization; import org.eclipse.leshan.server.security.Authorizer; import org.eclipse.leshan.server.security.SecurityChecker; import org.eclipse.leshan.server.security.SecurityInfo; @@ -45,38 +47,30 @@ public class TbLwM2MAuthorizer implements Authorizer { private final LwM2mClientContext clientContext; @Override - public Registration isAuthorized(UplinkRequest request, Registration registration, Identity senderIdentity) { - if (senderIdentity.isX509()) { - TbX509DtlsSessionInfo sessionInfo = sessionStorage.get(registration.getEndpoint()); - if (sessionInfo != null) { - if (sessionInfo.getX509CommonName().endsWith(senderIdentity.getX509CommonName())) { - clientContext.registerClient(registration, sessionInfo.getCredentials()); + public Authorization isAuthorized(UplinkRequest request, Registration registration, LwM2mPeer sender) { + SecurityInfo expectedSecurityInfo = null; + if (securityStore != null) expectedSecurityInfo = securityStore.getByEndpoint(registration.getEndpoint()); + if (securityChecker.checkSecurityInfo(registration.getEndpoint(), sender, expectedSecurityInfo)) { + if (sender.getIdentity() instanceof X509Identity) { + TbX509DtlsSessionInfo sessionInfo = sessionStorage.get(registration.getEndpoint()); + if (sessionInfo != null) { // X509 certificate is valid and matches endpoint. - return registration; - } else { - // X509 certificate is not valid. - return null; + clientContext.registerClient(registration, sessionInfo.getCredentials()); } } - // If session info is not found, this may be the trusted certificate, so we still need to check all other options below. - } - SecurityInfo expectedSecurityInfo; try { - expectedSecurityInfo = securityStore.getByEndpoint(registration.getEndpoint()); if (expectedSecurityInfo != null && expectedSecurityInfo.usePSK() && expectedSecurityInfo.getEndpoint().equals(SecurityMode.NO_SEC.toString()) - && expectedSecurityInfo.getIdentity().equals(SecurityMode.NO_SEC.toString()) + && expectedSecurityInfo.getPskIdentity().equals(SecurityMode.NO_SEC.toString()) && Arrays.equals(SecurityMode.NO_SEC.toString().getBytes(), expectedSecurityInfo.getPreSharedKey())) { - expectedSecurityInfo = null; + return Authorization.declined(); } } catch (LwM2MAuthException e) { log.info("Registration failed: FORBIDDEN, endpointId: [{}]", registration.getEndpoint()); - return null; + return Authorization.declined(); } - if (securityChecker.checkSecurityInfo(registration.getEndpoint(), senderIdentity, expectedSecurityInfo)) { - return registration; + return Authorization.approved(); } else { - securityStore.remove(registration.getEndpoint(), registration.getId()); - return null; + return Authorization.declined(); } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/AbstractLwM2mTransportResource.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/AbstractLwM2mTransportResource.java index d757c91648..dd387f5264 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/AbstractLwM2mTransportResource.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/AbstractLwM2mTransportResource.java @@ -18,12 +18,13 @@ package org.thingsboard.server.transport.lwm2m.server; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.leshan.core.californium.LwM2mCoapResource; +import org.eclipse.leshan.core.californium.identity.IdentityHandlerProvider; @Slf4j public abstract class AbstractLwM2mTransportResource extends LwM2mCoapResource { public AbstractLwM2mTransportResource(String name) { - super(name); + super(name, new IdentityHandlerProvider()); } @Override 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 5fb36f6f55..5c2045f61a 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 @@ -15,16 +15,22 @@ */ package org.thingsboard.server.transport.lwm2m.server; +import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.elements.config.Configuration; import org.eclipse.californium.scandium.config.DtlsConfig; -import org.eclipse.californium.scandium.config.DtlsConnectorConfig; import org.eclipse.californium.scandium.dtls.cipher.CipherSuite; +import org.eclipse.leshan.core.endpoint.Protocol; import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; 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.eclipse.leshan.server.LeshanServer; +import org.eclipse.leshan.server.LeshanServerBuilder; +import org.eclipse.leshan.server.californium.LwM2mPskStore; +import org.eclipse.leshan.server.californium.endpoint.CaliforniumServerEndpointsProvider; +import org.eclipse.leshan.server.californium.endpoint.coap.CoapServerProtocolProvider; +import org.eclipse.leshan.server.californium.endpoint.coaps.CoapsServerProtocolProvider; +import org.eclipse.leshan.server.registration.RegistrationStore; import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.ota.OtaPackageDataCache; @@ -37,13 +43,11 @@ 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.LwM2mUplinkMsgHandler; -import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; -import jakarta.annotation.PreDestroy; +import java.net.InetSocketAddress; import java.security.cert.X509Certificate; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CURVES_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_ROLE; @@ -64,12 +68,13 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { public static final CipherSuite[] RPK_OR_X509_CIPHER_SUITES = {TLS_PSK_WITH_AES_128_CCM_8, TLS_PSK_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256}; public static final CipherSuite[] PSK_CIPHER_SUITES = {TLS_PSK_WITH_AES_128_CCM_8, TLS_PSK_WITH_AES_128_CBC_SHA256}; +// public static final CipherSuite[] PSK_CIPHER_SUITES = {TLS_PSK_WITH_AES_128_CCM_8}; private final LwM2mTransportContext context; private final LwM2MTransportServerConfig config; private final OtaPackageDataCache otaPackageDataCache; private final LwM2mUplinkMsgHandler handler; - private final CaliforniumRegistrationStore registrationStore; + private final RegistrationStore registrationStore; private final TbSecurityStore securityStore; private final TbLwM2MDtlsCertificateVerifier certificateVerifier; private final TbLwM2MAuthorizer authorizer; @@ -88,7 +93,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { * "coap://host:port/{path}/{token}/{nameFile}" */ LwM2mTransportCoapResource otaCoapResource = new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE); - this.server.coap().getServer().add(otaCoapResource); +// this.server.coap().getServer().add(otaCoapResource); this.context.setServer(server); this.startLhServer(); } @@ -117,54 +122,128 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { private LeshanServer getLhServer() { LeshanServerBuilder builder = new LeshanServerBuilder(); - builder.setLocalAddress(config.getHost(), config.getPort()); - builder.setLocalSecureAddress(config.getSecureHost(), config.getSecurePort()); - builder.setDecoder(new DefaultLwM2mDecoder()); - /* Use a magic converter to support bad type send by the UI. */ - builder.setEncoder(new DefaultLwM2mEncoder(LwM2mValueConverterImpl.getInstance())); - - /* Create CoAP Config */ - builder.setCoapConfig(getCoapConfig(config.getPort(), config.getSecurePort(), config)); /* Define model provider (Create Models )*/ builder.setObjectModelProvider(modelProvider); + /* Set securityStore with new registrationStore */ builder.setSecurityStore(securityStore); builder.setRegistrationStore(registrationStore); + + // Create Californium Endpoints Provider: + // ------------------ + // Create Server Endpoints Provider + CaliforniumServerEndpointsProvider.Builder endpointsBuilder = new CaliforniumServerEndpointsProvider.Builder( + // Add coap Protocol support + new CoapServerProtocolProvider(), + + // Add coaps/dtls protocol support + new CoapsServerProtocolProvider(c -> { + if (this.config.getSslCredentials() != null) { + c.setAdvancedCertificateVerifier(certificateVerifier); + c.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, RPK_OR_X509_CIPHER_SUITES); + } else { + log.info("Unable to load X509 files for LWM2MServer"); + LwM2mPskStore lwM2mPskStore = new LwM2mPskStore(securityStore, registrationStore); + c.setAdvancedPskStore(lwM2mPskStore); + c.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, PSK_CIPHER_SUITES); + } + })); + + // Create Californium Configuration + Configuration serverCoapConfig = endpointsBuilder.createDefaultConfiguration(); + getCoapConfig(serverCoapConfig, config.getPort(), config.getSecurePort(), config); + + // Set some DTLS stuff + + serverCoapConfig.setTransient(DTLS_RECOMMENDED_CURVES_ONLY); + serverCoapConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, config.isRecommendedSupportedGroups()); + + serverCoapConfig.setTransient(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY); + serverCoapConfig.set(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, config.isRecommendedCiphers()); + + serverCoapConfig.set(DTLS_RETRANSMISSION_TIMEOUT, config.getDtlsRetransmissionTimeout(), MILLISECONDS); + serverCoapConfig.set(DTLS_ROLE, SERVER_ONLY); + serverCoapConfig.setTransient(DtlsConfig.DTLS_CONNECTION_ID_LENGTH); + /** + * "Control usage of DTLS connection ID.", // + * "- 'on' to activate Connection ID support ", // + * " (same as -cid 6)", // + * "- 'off' to deactivate it", // + * "- Positive value define the size in byte of CID generated.", // + * "- 0 value means we accept to use CID but will not generated one for foreign peer.", // + * "Default: on" + */ + int cid = 6; + serverCoapConfig.set(DtlsConfig.DTLS_CONNECTION_ID_LENGTH, cid); + + /* Create DTLS Config */ - DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(getCoapConfig(config.getPort(), config.getSecurePort(), config)); + this.setServerWithCredentials(builder); + + +// DtlsConnectorConfig dtlsConfig; +// try { +// dtlsConfig = dtlsConfigBuilder.build(); +// } catch (IllegalStateException e) { +// log.warn("Unable to create DTLS config for endpont {}.", endpointUri.toString(), e); +// return null; +// } +// +// Connector dTLSConnector = new DTLSConnector(dtlsConfig); + +// endpointsBuilder.setConnector(dTLSConnector); +// endpointsBuilder.setConfiguration(serverCoapConfig); - dtlsConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, config.isRecommendedSupportedGroups()); - dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, config.isRecommendedCiphers()); - dtlsConfig.set(DTLS_RETRANSMISSION_TIMEOUT, config.getDtlsRetransmissionTimeout(), MILLISECONDS); - dtlsConfig.set(DTLS_ROLE, SERVER_ONLY); +// endpointsBuilder.setLoggingTag(String.format("[%s]", "/" + "options.getUriPathString()")); +// endpointsBuilder.setEndpointContextMatcher(new Lwm2mEndpointContextMatcher()); /* Create credentials */ - this.setServerWithCredentials(builder, dtlsConfig); - /* Set DTLS Config */ - builder.setDtlsConfig(dtlsConfig); - /* Create LWM2M server */ + + + // Set Californium Configuration + endpointsBuilder.setConfiguration(serverCoapConfig); + + + // Create CoAP endpoint + InetSocketAddress coapAddr = new InetSocketAddress(config.getHost(), config.getPort()); + endpointsBuilder.addEndpoint(coapAddr, Protocol.COAP); + + // Create CoAP over DTLS endpoint + InetSocketAddress coapsAddr = new InetSocketAddress(config.getSecureHost(), config.getSecurePort()); + endpointsBuilder.addEndpoint(coapsAddr, Protocol.COAPS); + + + builder.setDecoder(new DefaultLwM2mDecoder(true)); + builder.setEncoder(new DefaultLwM2mEncoder(true)); +// builder.setDecoder(new DefaultLwM2mDecoder()); +// /* Use a magic converter to support bad type send by the UI. */ +// builder.setEncoder(new DefaultLwM2mEncoder(LwM2mValueConverterImpl.getInstance())); + + /* Create CoAP Config */ +// builder.setCoapConfig(getCoapConfig(config.getPort(), config.getSecurePort(), config)); + + + // Create LWM2M server + builder.setEndpointsProviders(endpointsBuilder.build()); return builder.build(); } - private void setServerWithCredentials(LeshanServerBuilder builder, DtlsConnectorConfig.Builder dtlsConfig) { + private void setServerWithCredentials(LeshanServerBuilder builder) { +// private void setServerWithCredentials(LeshanServerBuilder builder) { if (this.config.getSslCredentials() != null) { SslCredentials sslCredentials = this.config.getSslCredentials(); builder.setPublicKey(sslCredentials.getPublicKey()); builder.setPrivateKey(sslCredentials.getPrivateKey()); builder.setCertificateChain(sslCredentials.getCertificateChain()); - dtlsConfig.setAdvancedCertificateVerifier(certificateVerifier); builder.setAuthorizer(authorizer); - dtlsConfig.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, RPK_OR_X509_CIPHER_SUITES); } else { /* by default trust all */ builder.setTrustedCertificates(new X509Certificate[0]); - log.info("Unable to load X509 files for LWM2MServer"); - dtlsConfig.setAsList(DtlsConfig.DTLS_CIPHER_SUITES, PSK_CIPHER_SUITES); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MNetworkConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MNetworkConfig.java index 569198f50f..eafe11d9dc 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MNetworkConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MNetworkConfig.java @@ -27,8 +27,8 @@ import static org.eclipse.californium.core.config.CoapConfig.DEFAULT_BLOCKWISE_S public class LwM2MNetworkConfig { - public static Configuration getCoapConfig(Integer serverPortNoSec, Integer serverSecurePort, LwM2MTransportServerConfig config) { - Configuration coapConfig = new Configuration(); + public static Configuration getCoapConfig(Configuration coapConfig, Integer serverPortNoSec, Integer serverSecurePort, LwM2MTransportServerConfig config) { +// Configuration coapConfig = new Configuration(); coapConfig.set(CoapConfig.COAP_PORT, serverPortNoSec); coapConfig.set(CoapConfig.COAP_SECURE_PORT, serverSecurePort); /** @@ -107,4 +107,4 @@ public class LwM2MNetworkConfig { return coapConfig; } -} \ No newline at end of file +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java index b894861576..a29a37c766 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -16,7 +16,7 @@ package org.thingsboard.server.transport.lwm2m.server; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.observation.SingleObservation; @@ -32,7 +32,6 @@ import org.eclipse.leshan.server.send.SendListener; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import java.util.Collection; -import java.util.Map; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertObjectIdToVersionedId; @@ -128,10 +127,15 @@ public class LwM2mServerListener { public final SendListener sendListener = new SendListener() { @Override - public void dataReceived(Registration registration, Map map, SendRequest sendRequest) { + public void dataReceived(Registration registration, TimestampedLwM2mNodes data, SendRequest request) { if (registration != null) { - service.onUpdateValueWithSendRequest(registration, sendRequest); + service.onUpdateValueWithSendRequest(registration, request); } } + + @Override + public void onError(Registration registration, String errorMessage, Exception error) { + + } }; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContext.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContext.java index f00d475a4b..91ae14108b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContext.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContext.java @@ -17,7 +17,7 @@ package org.thingsboard.server.transport.lwm2m.server; import lombok.Getter; import lombok.Setter; -import org.eclipse.leshan.server.californium.LeshanServer; +import org.eclipse.leshan.server.LeshanServer; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java index 3dd13cd171..c6716b6bf4 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java @@ -16,7 +16,7 @@ package org.thingsboard.server.transport.lwm2m.server; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.model.DefaultDDFFileValidator; +import org.eclipse.leshan.core.LwM2m; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; @@ -107,7 +107,7 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { @Override public ObjectModel getObjectModel(int objectId) { - String version = registration.getSupportedVersion(objectId); + String version = String.valueOf(registration.getSupportedVersion(objectId)); if (version != null) { return this.getObjectModelDynamic(objectId, version); } @@ -116,10 +116,10 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { @Override public Collection getObjectModels() { - Map supportedObjects = this.registration.getSupportedObject(); + Map supportedObjects = this.registration.getSupportedObject(); Collection result = new ArrayList<>(supportedObjects.size()); - for (Map.Entry supportedObject : supportedObjects.entrySet()) { - ObjectModel objectModel = this.getObjectModelDynamic(supportedObject.getKey(), supportedObject.getValue()); + for (Map.Entry supportedObject : supportedObjects.entrySet()) { + ObjectModel objectModel = this.getObjectModelDynamic(supportedObject.getKey(), String.valueOf(supportedObject.getValue())); if (objectModel != null) { result.add(objectModel); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 9b63c2f2b9..b26d8f03e8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -20,7 +20,8 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.link.LinkParamValue; +import org.eclipse.leshan.core.LwM2m; +import org.eclipse.leshan.core.link.attributes.Attribute; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; @@ -271,7 +272,7 @@ public class LwM2mClient { public ResourceModel getResourceModel(String pathIdVer, LwM2mModelProvider modelProvider) { LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer)); - String verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId()); + String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId())); String verRez = getVerFromPathIdVerOrId(pathIdVer); return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration) .getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()) : null; @@ -289,7 +290,7 @@ public class LwM2mClient { public ObjectModel getObjectModel(String pathIdVer, LwM2mModelProvider modelProvider) { try { LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer)); - String verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId()); + String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId())); String verRez = getVerFromPathIdVerOrId(pathIdVer); return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration) .getObjectModel(pathIds.getObjectId()) : null; @@ -370,12 +371,12 @@ public class LwM2mClient { public String isValidObjectVersion(String path) { LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(path)); - String verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId()); + LwM2m.Version verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId()); if (verSupportedObject == null) { return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId()); } else { String verRez = getVerFromPathIdVerOrId(path); - if (verRez == null || !verRez.equals(verSupportedObject)) { + if (verRez == null || !verRez.equals(verSupportedObject.toString())) { return String.format("Specified resource id %s is not valid version! Must be version: %s", path, verSupportedObject); } } @@ -432,16 +433,12 @@ public class LwM2mClient { private static Set clientSupportContentFormat(Registration registration) { Set contentFormats = new HashSet<>(); contentFormats.add(ContentFormat.DEFAULT); - LinkParamValue ct = Arrays.stream(registration.getObjectLinks()) + Attribute ct = Arrays.stream(registration.getObjectLinks()) .filter(link -> link.getUriReference().equals("/")) .findFirst() - .map(link -> link.getLinkParams().get("ct")).orElse(null); + .map(link -> link.getAttributes().get("ct")).orElse(null); if (ct != null) { - Set codes = Stream.of(ct.getUnquoted().replaceAll("\"", "").split(" ", -1)) - .map(String::trim) - .map(Integer::parseInt) - .map(ContentFormat::fromCode) - .collect(Collectors.toSet()); + Set codes = Stream.of(ct.getValue()).collect(Collectors.toSet()); contentFormats.addAll(codes); } return contentFormats; 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 09ecf500b6..e124b8142d 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 @@ -15,12 +15,15 @@ */ package org.thingsboard.server.transport.lwm2m.server.downlink; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.LwM2m; -import org.eclipse.leshan.core.attributes.Attribute; -import org.eclipse.leshan.core.attributes.AttributeSet; import org.eclipse.leshan.core.link.Link; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeModel; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; @@ -75,8 +78,6 @@ import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogServic import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcWriteCompositeRequest; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.util.Arrays; import java.util.Collection; import java.util.Date; @@ -86,15 +87,16 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; -import static org.eclipse.leshan.core.attributes.Attribute.GREATER_THAN; -import static org.eclipse.leshan.core.attributes.Attribute.LESSER_THAN; -import static org.eclipse.leshan.core.attributes.Attribute.MAXIMUM_PERIOD; -import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD; -import static org.eclipse.leshan.core.attributes.Attribute.STEP; +import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.GREATER_THAN; +import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.LESSER_THAN; +import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.MAXIMUM_PERIOD; +import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.MINIMUM_PERIOD; +import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.STEP; import static org.eclipse.leshan.core.model.ResourceModel.Type.OBJLNK; import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody; @@ -148,8 +150,9 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im @Override public void sendReadCompositeRequest(LwM2mClient client, TbLwM2MReadCompositeRequest request, - DownlinkRequestCallback callback, ContentFormat compositeContentFormat) { + DownlinkRequestCallback callback) { try { + ContentFormat compositeContentFormat = this.findFirstContentFormatForComposite(client.getClientSupportContentFormats()); ReadCompositeRequest downlink = new ReadCompositeRequest(compositeContentFormat, compositeContentFormat, request.getObjectIds()); sendCompositeRequest(client, downlink, this.config.getTimeout(), callback); } catch (InvalidRequestException e) { @@ -193,8 +196,8 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } @Override - public void sendDiscoverAllRequest(LwM2mClient client, TbLwM2MDiscoverAllRequest request, DownlinkRequestCallback> callback) { - callback.onSuccess(request, Arrays.asList(client.getRegistration().getSortedObjectLinks())); + public void sendDiscoverAllRequest(LwM2mClient client, TbLwM2MDiscoverAllRequest request, DownlinkRequestCallback> callback) { + callback.onSuccess(request, Arrays.stream(client.getRegistration().getSortedObjectLinks()).map(Link::toCoreLinkFormat).collect(Collectors.toList())); } @Override @@ -279,13 +282,13 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im throw new IllegalArgumentException("Attributes to write are not specified!"); } ObjectAttributes params = request.getAttributes(); - List attributes = new LinkedList<>(); + List> attributes = new LinkedList<>(); addAttribute(attributes, MAXIMUM_PERIOD, params.getPmax()); addAttribute(attributes, MINIMUM_PERIOD, params.getPmin()); addAttribute(attributes, GREATER_THAN, params.getGt()); addAttribute(attributes, LESSER_THAN, params.getLt()); addAttribute(attributes, STEP, params.getSt()); - AttributeSet attributeSet = new AttributeSet(attributes); + LwM2mAttributeSet attributeSet = new LwM2mAttributeSet(attributes); sendSimpleRequest(client, new WriteAttributesRequest(request.getObjectId(), attributeSet), request.getTimeout(), callback); } catch (InvalidRequestException e) { callback.onValidationError(request.toString(), e.getMessage()); @@ -338,9 +341,10 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im @Override public void sendWriteCompositeRequest(LwM2mClient client, RpcWriteCompositeRequest rpcWriteCompositeRequest, - DownlinkRequestCallback callback, ContentFormat contentFormatComposite) { + DownlinkRequestCallback callback) { try { - WriteCompositeRequest downlink = new WriteCompositeRequest(contentFormatComposite, rpcWriteCompositeRequest.getNodes()); + ContentFormat compositeContentFormat = this.findFirstContentFormatForComposite(client.getClientSupportContentFormats()); + WriteCompositeRequest downlink = new WriteCompositeRequest(compositeContentFormat, rpcWriteCompositeRequest.getNodes()); //TODO: replace config.getTimeout(); sendWriteCompositeRequest(client, downlink, this.config.getTimeout(), callback); } catch (InvalidRequestException e) { @@ -570,17 +574,18 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } } - private static void addAttribute(List attributes, String attributeName, T value) { - addAttribute(attributes, attributeName, value, null, null); + private static void addAttribute(List> attributes, LwM2mAttributeModel attribute, T value) { + addAttribute(attributes, attribute, value, null, null); } - private static void addAttribute(List attributes, String attributeName, T value, Function converter) { - addAttribute(attributes, attributeName, value, null, converter); + private static void addAttribute(List> attributes, LwM2mAttributeModel attribute, T value, Function converter) { + addAttribute(attributes, attribute, value, null, converter); } - private static void addAttribute(List attributes, String attributeName, T value, Predicate filter, Function converter) { + private static void addAttribute(List> attributes, LwM2mAttributeModel attributeName, T value, Predicate filter, Function converter) { if (value != null && ((filter == null) || filter.test(value))) { - attributes.add(new Attribute(attributeName, converter != null ? converter.apply(value) : value)); + T valueConvert = (T) converter != null ? (T) converter.apply(value) : value; + attributes.add(new LwM2mAttribute<>(attributeName, valueConvert)); } } @@ -620,7 +625,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } else if (OPAQUE.equals(resourceModel.type)) { return ContentFormat.OPAQUE; } else { - return findFirst(client.getClientSupportContentFormats(), client.getDefaultContentFormat(), ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON); + return findFirstContentFormatForComp(client.getClientSupportContentFormats(), client.getDefaultContentFormat(), ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON); } } else { return getContentFormatForComplex(client); @@ -634,7 +639,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im if (LwM2m.LwM2mVersion.V1_0.equals(client.getRegistration().getLwM2mVersion())) { return ContentFormat.TLV; } else if (LwM2m.LwM2mVersion.V1_1.equals(client.getRegistration().getLwM2mVersion())) { - ContentFormat result = findFirst(client.getClientSupportContentFormats(), null, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON, ContentFormat.TLV, ContentFormat.JSON); + ContentFormat result = findFirstContentFormatForComp(client.getClientSupportContentFormats(), null, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON, ContentFormat.TLV, ContentFormat.JSON); if (result != null) { return result; } else { @@ -644,16 +649,6 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im throw new RuntimeException("The version " + client.getRegistration().getLwM2mVersion() + " is not supported!"); } } - - private static ContentFormat findFirst(Set supported, ContentFormat defaultValue, ContentFormat... desiredFormats) { - for (ContentFormat contentFormat : desiredFormats) { - if (supported.contains(contentFormat)) { - return contentFormat; - } - } - return defaultValue; - } - private String toString(R request) { try { return request != null ? request.toString() : ""; @@ -662,4 +657,29 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im return request.getClass().getSimpleName(); } } + + private ContentFormat findFirstContentFormatForComposite (Set clientSupportContentFormats) { + ContentFormat contentFormat = findFirstContentFormatForComp(clientSupportContentFormats, null, ContentFormat.SENML_JSON, ContentFormat.SENML_CBOR); + if (contentFormat != null) { + return contentFormat; + } else { + throw new RuntimeException("This device does not support Composite Operation"); + } + } + private static ContentFormat findFirstContentFormatForComp(Set clientSupportContentFormats, ContentFormat defaultValue, ContentFormat... desiredFormats) { + AtomicReference compositeContentFormat = new AtomicReference<>(); + clientSupportContentFormats.forEach(c -> { + if (c instanceof Collection) { + for (ContentFormat contentFormat : desiredFormats) { + if (((Collection) c).contains(contentFormat)) { + compositeContentFormat.set(contentFormat); + break; + } + } + } else if (compositeContentFormat.get() == null && c instanceof ContentFormat) { + compositeContentFormat.set(Arrays.stream(desiredFormats).filter(f -> f.equals(c)).findFirst().get()); + } + }); + return compositeContentFormat.get() != null ? compositeContentFormat.get() : defaultValue; + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java index 90d4006fb4..a9d49e6dae 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.transport.lwm2m.server.downlink; -import org.eclipse.leshan.core.link.Link; -import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.CreateRequest; import org.eclipse.leshan.core.request.DeleteRequest; import org.eclipse.leshan.core.request.DiscoverRequest; @@ -48,7 +46,7 @@ public interface LwM2mDownlinkMsgHandler { void sendReadRequest(LwM2mClient client, TbLwM2MReadRequest request, DownlinkRequestCallback callback); - void sendReadCompositeRequest(LwM2mClient client, TbLwM2MReadCompositeRequest request, DownlinkRequestCallback callback, ContentFormat contentFormatComposite); + void sendReadCompositeRequest(LwM2mClient client, TbLwM2MReadCompositeRequest request, DownlinkRequestCallback callback); void sendObserveRequest(LwM2mClient client, TbLwM2MObserveRequest request, DownlinkRequestCallback callback); @@ -64,13 +62,13 @@ public interface LwM2mDownlinkMsgHandler { void sendDiscoverRequest(LwM2mClient client, TbLwM2MDiscoverRequest request, DownlinkRequestCallback callback); - void sendDiscoverAllRequest(LwM2mClient client, TbLwM2MDiscoverAllRequest request, DownlinkRequestCallback> callback); + void sendDiscoverAllRequest(LwM2mClient client, TbLwM2MDiscoverAllRequest request, DownlinkRequestCallback> callback); void sendWriteAttributesRequest(LwM2mClient client, TbLwM2MWriteAttributesRequest request, DownlinkRequestCallback callback); void sendWriteReplaceRequest(LwM2mClient client, TbLwM2MWriteReplaceRequest request, DownlinkRequestCallback callback); - void sendWriteCompositeRequest(LwM2mClient client, RpcWriteCompositeRequest nodes, DownlinkRequestCallback callback, ContentFormat contentFormatComposite); + void sendWriteCompositeRequest(LwM2mClient client, RpcWriteCompositeRequest nodes, DownlinkRequestCallback callback); void sendWriteUpdateRequest(LwM2mClient client, TbLwM2MWriteUpdateRequest request, DownlinkRequestCallback callback); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java index 5fd20da37d..c725d4f070 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java @@ -23,7 +23,6 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mPath; -import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; @@ -75,6 +74,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertValueByTypeResource; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; @Slf4j @@ -155,22 +155,16 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { throw new IllegalArgumentException("Unsupported operation: " + operationType.name()); } } else if (operationType.isComposite()) { - ContentFormat contentFormatComposite = this.getCompositeContentFormat(client); - if (contentFormatComposite != null) { switch (operationType) { case READ_COMPOSITE: - sendReadCompositeRequest(client, rpcRequest, contentFormatComposite); + sendReadCompositeRequest(client, rpcRequest); break; case WRITE_COMPOSITE: - sendWriteCompositeRequest(client, rpcRequest, contentFormatComposite); + sendWriteCompositeRequest(client, rpcRequest); break; default: throw new IllegalArgumentException("Unsupported operation: " + operationType.name()); } - } else { - this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), - ResponseCode.INTERNAL_SERVER_ERROR, "This device does not support Composite Operation"); - } } else { switch (operationType) { case OBSERVE_CANCEL_ALL: @@ -202,12 +196,12 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { downlinkHandler.sendReadRequest(client, request, rpcCallback); } - private void sendReadCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, ContentFormat contentFormatComposite) { + private void sendReadCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) { String[] versionedIds = getIdsFromParameters(client, requestMsg); TbLwM2MReadCompositeRequest request = TbLwM2MReadCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MReadCompositeCallback(uplinkHandler, logService, client, versionedIds); var rpcCallback = new RpcReadResponseCompositeCallback(transportService, client, requestMsg, mainCallback); - downlinkHandler.sendReadCompositeRequest(client, request, rpcCallback, contentFormatComposite); + downlinkHandler.sendReadCompositeRequest(client, request, rpcCallback); } private void sendObserveRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { @@ -243,7 +237,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { private void sendWriteAttributesRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { RpcWriteAttributesRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteAttributesRequest.class); - TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(versionedId) + TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(versionedId) .attributes(requestBody.getAttributes()) .timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MWriteAttributesCallback(logService, client, versionedId); @@ -298,54 +292,89 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { * nodes.put("/1/0/2", 100); * nodes.put("/5/0/1", "coap://localhost:5685"); */ - private void sendWriteCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, ContentFormat contentFormatComposite) { + private void sendWriteCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) { RpcWriteCompositeRequest rpcWriteCompositeRequest = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteCompositeRequest.class); - Map validNodes = validateNodes(client, rpcWriteCompositeRequest.getNodes()); + Map validNodes = validateCompositesNodes(client, rpcWriteCompositeRequest.getNodes()); if (validNodes.size() > 0) { rpcWriteCompositeRequest.setNodes(validNodes); var mainCallback = new TbLwM2MWriteResponseCompositeCallback(uplinkHandler, logService, client, null); var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback); - downlinkHandler.sendWriteCompositeRequest(client, rpcWriteCompositeRequest, rpcCallback, contentFormatComposite); + downlinkHandler.sendWriteCompositeRequest(client, rpcWriteCompositeRequest, rpcCallback); } else { throw new IllegalArgumentException(String.format("nodes: %s is not validate value", rpcWriteCompositeRequest.getNodes().toString())); } } - private Map validateNodes(LwM2mClient client, Map nodes) { + private Map validateCompositesNodes(LwM2mClient client, Map nodes) { Map newNodes = new LinkedHashMap<>(); nodes.forEach((key, value) -> { - String versionedId; + String versionedId = null; + LwM2mPath path = null; try { - LwM2mPath path = new LwM2mPath(fromVersionedIdToObjectId(key)); - if (path.isResource() || path.isResourceInstance()) { - versionedId = key; - } - else { - throw new IllegalArgumentException(String.format("nodes: %s is not validate value. " + - "The WriteComposite operation is only used for SingleResources or/and ResourceInstance.", nodes.toString())); - } + path = new LwM2mPath(fromVersionedIdToObjectId(key)); } catch (Exception e) { versionedId = clientContext.getObjectIdByKeyNameFromProfile(client, key); } - // validate value. Must be only primitive, not JsonObject or JsonArray - try { - JsonElement element = JsonUtils.parse(value.toString()); - if (!element.isJsonNull() && !element.isJsonPrimitive()) { + + if (versionedId == null) { + if (path.isResourceInstance()) { + setValueToCompositeNodes(client, newNodes, nodes, key, value.toString()); + } else if (path.isResource()) { + validateResource(client, newNodes, nodes, key , value); + } else if (path.isObjectInstance() && value instanceof Map) { + ((Map) value).forEach((k, v) -> { + validateResource(client, newNodes, nodes, validateResourceId (key, k.toString(), nodes), v); + }); + } else { throw new IllegalArgumentException(String.format("nodes: %s is not validate value. " + - "The WriteComposite operation is only used for SingleResources or/and ResourceInstance.", nodes.toString())); - } - else if (versionedId != null) { - newNodes.put(fromVersionedIdToObjectId(versionedId), value); - } - } catch (JsonSyntaxException jse) { - if (versionedId != null) { - newNodes.put(fromVersionedIdToObjectId(versionedId), value); + "The WriteComposite operation is only used for SingleResources or/and ResourceInstance.", nodes)); } + } else { + setValueToCompositeNodes(client, newNodes, nodes, versionedId, value.toString()); } }); return newNodes; } + private void validateResource(LwM2mClient client, Map newNodes, Map nodes, String resourceId , Object value) { + if (value instanceof Map) { + ((Map) value).forEach((k, v) -> { + setValueToCompositeNodes(client, newNodes, nodes, validateResourceId (resourceId, k.toString(), nodes), v.toString()); + }); + } else { + setValueToCompositeNodes(client, newNodes, nodes, resourceId, value.toString()); + } + } + + private String validateResourceId (String key, String id, Map nodes) { + try { + Integer.parseInt(id); + return key + "/" + id; + } catch (NumberFormatException e) { + throw new IllegalArgumentException(String.format("nodes: %s is not validate value. " + + "The WriteComposite operation is only used for SingleResources or/and ResourceInstance.", nodes.toString())); + } + } + + private void setValueToCompositeNodes (LwM2mClient client, Map newNodes, Map nodes, String versionedId , String value) { + // validate value. Must be only primitive, not JsonObject or JsonArray + try { + JsonElement element = JsonUtils.parse(value); + if (!element.isJsonNull() && !element.isJsonPrimitive()) { + throw new IllegalArgumentException(String.format("nodes: %s is not validate value. " + + "The WriteComposite operation is only used for SingleResources or/and ResourceInstance.", nodes.toString())); + } + // convert value from JsonPrimitive() to resource/ResourceInstance type + ResourceModel resourceModel = client.getResourceModel(versionedId, modelProvider); + Object newValue = convertValueByTypeResource(value, resourceModel.type, versionedId); + + // add new value after convert + newNodes.put(fromVersionedIdToObjectId(versionedId), newValue); + } catch (JsonSyntaxException jse) { + newNodes.put(fromVersionedIdToObjectId(versionedId), value); + } + } + private void sendCancelObserveRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { TbLwM2MCancelObserveRequest downlink = TbLwM2MCancelObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MCancelObserveCallback(logService, client, versionedId); @@ -412,16 +441,4 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse) { log.info("[{}] toServerRpcResponse", toServerResponse); } - - private ContentFormat getCompositeContentFormat(LwM2mClient client) { - if (client.getClientSupportContentFormats().contains(ContentFormat.SENML_JSON)) { - return ContentFormat.SENML_JSON; - } - else if (client.getClientSupportContentFormats().contains(ContentFormat.SENML_CBOR)) { - return ContentFormat.SENML_CBOR; - } - else { - return null; - } - } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcDiscoverCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcDiscoverCallback.java index acb44fe55f..314d88fccb 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcDiscoverCallback.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcDiscoverCallback.java @@ -35,7 +35,7 @@ public class RpcDiscoverCallback extends RpcLwM2MDownlinkCallback serializeSuccessfulResponse(DiscoverResponse response) { - return Optional.of(serializer.serialize(response.getObjectLinks())); + return Optional.of(serializer.serializeCoreLinkFormat(response.getObjectLinks())); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemorySecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemorySecurityStore.java index cc96cea4b7..7709061e32 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemorySecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemorySecurityStore.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.server.store; import org.eclipse.leshan.core.SecurityMode; +import org.eclipse.leshan.core.peer.OscoreIdentity; import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; import org.eclipse.leshan.server.security.SecurityInfo; import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MSecurityInfo; @@ -83,25 +84,30 @@ public class TbInMemorySecurityStore implements TbEditableSecurityStore { } } + @Override + public SecurityInfo getByOscoreIdentity(OscoreIdentity oscoreIdentity) { + return null; + } + @Override public void put(TbLwM2MSecurityInfo tbSecurityInfo) throws NonUniqueSecurityInfoException { writeLock.lock(); try { String identity = null; if (tbSecurityInfo.getSecurityInfo() != null) { - identity = tbSecurityInfo.getSecurityInfo().getIdentity(); + identity = tbSecurityInfo.getSecurityInfo().getPskIdentity(); if (identity != null) { TbLwM2MSecurityInfo infoByIdentity = securityByIdentity.get(identity); if (infoByIdentity != null && !tbSecurityInfo.getSecurityInfo().getEndpoint().equals(infoByIdentity.getEndpoint())) { throw new NonUniqueSecurityInfoException("PSK Identity " + identity + " is already used"); } - securityByIdentity.put(tbSecurityInfo.getSecurityInfo().getIdentity(), tbSecurityInfo); + securityByIdentity.put(tbSecurityInfo.getSecurityInfo().getPskIdentity(), tbSecurityInfo); } } TbLwM2MSecurityInfo previous = securityByEp.put(tbSecurityInfo.getEndpoint(), tbSecurityInfo); if (previous != null && previous.getSecurityInfo() != null) { - String previousIdentity = previous.getSecurityInfo().getIdentity(); + String previousIdentity = previous.getSecurityInfo().getPskIdentity(); if (previousIdentity != null && !previousIdentity.equals(identity)) { securityByIdentity.remove(previousIdentity); } @@ -116,8 +122,8 @@ public class TbInMemorySecurityStore implements TbEditableSecurityStore { writeLock.lock(); try { TbLwM2MSecurityInfo securityInfo = securityByEp.remove(endpoint); - if (securityInfo != null && securityInfo.getSecurityInfo() != null && securityInfo.getSecurityInfo().getIdentity() != null) { - securityByIdentity.remove(securityInfo.getSecurityInfo().getIdentity()); + if (securityInfo != null && securityInfo.getSecurityInfo() != null && securityInfo.getSecurityInfo().getPskIdentity() != null) { + securityByIdentity.remove(securityInfo.getSecurityInfo().getPskIdentity()); } } finally { writeLock.unlock(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index 39c19c687b..40a281728b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -16,6 +16,8 @@ package org.thingsboard.server.transport.lwm2m.server.store; import org.eclipse.californium.core.coap.Token; +import org.eclipse.californium.core.network.serialization.UdpDataParser; +import org.eclipse.californium.core.network.serialization.UdpDataSerializer; import org.eclipse.californium.core.observe.ObservationStoreException; import org.eclipse.californium.elements.EndpointContext; import org.eclipse.leshan.core.Destroyable; @@ -23,18 +25,18 @@ import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Stoppable; import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.ObservationIdentifier; import org.eclipse.leshan.core.observation.SingleObservation; -import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.peer.LwM2mIdentity; import org.eclipse.leshan.core.util.NamedThreadFactory; import org.eclipse.leshan.core.util.Validate; -import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; +import org.eclipse.leshan.server.californium.observation.ObservationSerDes; import org.eclipse.leshan.server.redis.RedisRegistrationStore; -import org.eclipse.leshan.server.redis.serialization.IdentitySerDes; -import org.eclipse.leshan.server.redis.serialization.ObservationSerDes; import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes; import org.eclipse.leshan.server.registration.Deregistration; import org.eclipse.leshan.server.registration.ExpirationListener; import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.registration.RegistrationStore; import org.eclipse.leshan.server.registration.RegistrationUpdate; import org.eclipse.leshan.server.registration.UpdatedRegistration; import org.slf4j.Logger; @@ -63,7 +65,7 @@ import java.util.concurrent.locks.Lock; import static java.nio.charset.StandardCharsets.UTF_8; -public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationStore, Startable, Stoppable, Destroyable { +public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startable, Stoppable, Destroyable { /** Default time in seconds between 2 cleaning tasks (used to remove expired registration). */ public static final long DEFAULT_CLEAN_PERIOD = 60; public static final int DEFAULT_CLEAN_LIMIT = 500; @@ -83,6 +85,8 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto private static final byte[] EXP_EP = "EXP:EP".getBytes(UTF_8); // a sorted set used for registration expiration // (expiration date, Endpoint) + private final RegistrationSerDes registrationSerDes = new RegistrationSerDes(); + private final ObservationSerDes observationSerDes = new ObservationSerDes(new UdpDataParser(), new UdpDataSerializer()); private final RedisConnectionFactory connectionFactory; // Listener use to notify when a registration expires @@ -159,7 +163,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto connection.set(regid_idx, registration.getEndpoint().getBytes(UTF_8)); byte[] addr_idx = toRegAddrKey(registration.getSocketAddress()); connection.set(addr_idx, registration.getEndpoint().getBytes(UTF_8)); - byte[] identity_idx = toRegIdentityKey(registration.getIdentity()); + byte[] identity_idx = toRegIdentityKey(registration.getClientTransportData().getIdentity()); connection.set(identity_idx, registration.getEndpoint().getBytes(UTF_8)); // Add or update expiration @@ -173,7 +177,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto if (!oldRegistration.getSocketAddress().equals(registration.getSocketAddress())) { removeAddrIndex(connection, oldRegistration); } - if (!oldRegistration.getIdentity().equals(registration.getIdentity())) { + if (!oldRegistration.getClientTransportData().getIdentity().equals(registration.getClientTransportData().getIdentity())) { removeIdentityIndex(connection, oldRegistration); } // remove old observation @@ -231,7 +235,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto if (!r.getSocketAddress().equals(updatedRegistration.getSocketAddress())) { removeAddrIndex(connection, r); } - if (!r.getIdentity().equals(updatedRegistration.getIdentity())) { + if (!r.getClientTransportData().getIdentity().equals(updatedRegistration.getClientTransportData().getIdentity())) { removeIdentityIndex(connection, r); } @@ -281,7 +285,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto } @Override - public Registration getRegistrationByIdentity(Identity identity) { + public Registration getRegistrationByIdentity(LwM2mIdentity identity) { Validate.notNull(identity); try (var connection = connectionFactory.getConnection()) { byte[] ep = connection.get(toRegIdentityKey(identity)); @@ -327,6 +331,26 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto } } + @Override + public Collection addObservation(String registrationId, Observation observation, boolean addIfAbsent) { + return null; + } + + @Override + public Observation getObservation(String registrationId, ObservationIdentifier observationId) { + return null; + } + + @Override + public Observation getObservation(ObservationIdentifier observationId) { + return null; + } + + @Override + public Observation removeObservation(String registrationId, ObservationIdentifier observationId) { + return null; + } + private Deregistration removeRegistration(RedisConnection connection, String registrationId, boolean removeOnlyIfNotAlive) { // fetch the client ep by registration ID index byte[] ep = connection.get(toRegIdKey(registrationId)); @@ -371,7 +395,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto } private void removeIdentityIndex(RedisConnection connection, Registration r) { - removeSecondaryIndex(connection, toRegIdentityKey(r.getIdentity()), r.getEndpoint()); + removeSecondaryIndex(connection, toRegIdentityKey(r.getClientTransportData().getIdentity()), r.getEndpoint()); } //TODO: JedisCluster didn't implement Transaction, maybe should use some advanced key creation strategies @@ -410,8 +434,8 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto return toKey(REG_EP_ADDR_IDX, addr.getAddress().toString() + ":" + addr.getPort()); } - private byte[] toRegIdentityKey(Identity identity) { - return toKey(REG_EP_IDENTITY, IdentitySerDes.serialize(identity).toString()); + private byte[] toRegIdentityKey(LwM2mIdentity identity) { + return toKey(REG_EP_IDENTITY, identity.toString()); } private byte[] toEndpointKey(String endpoint) { @@ -423,11 +447,11 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto } private byte[] serializeReg(Registration registration) { - return RegistrationSerDes.bSerialize(registration); + return registrationSerDes.bSerialize(registration); } private Registration deserializeReg(byte[] data) { - return RegistrationSerDes.deserialize(data); + return registrationSerDes.deserialize(data); } /* *************** Leshan Observation API **************** */ @@ -436,7 +460,7 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto * The observation is not persisted here, it is done by the Californium layer (in the implementation of the * org.eclipse.californium.core.observe.ObservationStore#add method) */ - @Override + public Collection addObservation(String registrationId, Observation observation) { List removed = new ArrayList<>(); try (var connection = connectionFactory.getConnection()) { @@ -458,9 +482,9 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto for (Observation obs : getObservations(connection, registrationId)) { //TODO: should be able to use CompositeObservation if (((SingleObservation)observation).getPath().equals(((SingleObservation)obs).getPath()) - && !Arrays.equals(observation.getId(), obs.getId())) { + && !observation.getId().equals(obs.getId())) { removed.add(obs); - unsafeRemoveObservation(connection, registrationId, obs.getId()); + unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); } } @@ -473,7 +497,6 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto return removed; } - @Override public Observation removeObservation(String registrationId, byte[] observationId) { try (var connection = connectionFactory.getConnection()) { @@ -490,7 +513,8 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto lock = redisLock.obtain(lockKey); lock.lock(); - Observation observation = build(get(new Token(observationId))); +// Observation observation = build(get(new Token(observationId))); + Observation observation = build(null); if (observation != null && registrationId.equals(observation.getRegistrationId())) { unsafeRemoveObservation(connection, registrationId, observationId); return observation; @@ -505,9 +529,9 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto } } - @Override public Observation getObservation(String registrationId, byte[] observationId) { - return build(get(new Token(observationId))); +// return build(get(new Token(observationId))); + return build(null); } @Override @@ -554,13 +578,11 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto /* *************** Californium ObservationStore API **************** */ - @Override public org.eclipse.californium.core.observe.Observation putIfAbsent(Token token, org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException { return add(obs, true); } - @Override public org.eclipse.californium.core.observe.Observation put(Token token, org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException { return add(obs, false); @@ -588,7 +610,8 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto if (previousValue == null || previousValue.length == 0) { connection.set(key, serializeObs); } else { - return deserializeObs(previousValue); +// return deserializeObs(previousValue); + return null; } } else { previousValue = connection.getSet(key, serializeObs); @@ -599,7 +622,8 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto // log any collisions if (previousValue != null && previousValue.length != 0) { - previousObservation = deserializeObs(previousValue); +// previousObservation = deserializeObs(previousValue); + previousObservation = null; LOG.warn( "Token collision ? observation from request [{}] will be replaced by observation from request [{}] ", previousObservation.getRequest(), obs.getRequest()); @@ -613,7 +637,6 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto return previousObservation; } - @Override public void remove(Token token) { try (var connection = connectionFactory.getConnection()) { byte[] tokenKey = toKey(OBS_TKN, token.getBytes()); @@ -623,7 +646,8 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto if (serializedObs == null) return; - org.eclipse.californium.core.observe.Observation obs = deserializeObs(serializedObs); +// org.eclipse.californium.core.observe.Observation obs = deserializeObs(serializedObs); + org.eclipse.californium.core.observe.Observation obs = null; String registrationId = ObserveUtil.extractRegistrationId(obs); Registration registration = getRegistration(connection, registrationId); if (registration == null) { @@ -649,14 +673,14 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto } - @Override public org.eclipse.californium.core.observe.Observation get(Token token) { try (var connection = connectionFactory.getConnection()) { byte[] obs = connection.get(toKey(OBS_TKN, token.getBytes())); if (obs == null) { return null; } else { - return deserializeObs(obs); +// return deserializeObs(obs); + return null; } } } @@ -699,24 +723,26 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto return removed; } - @Override public void setContext(Token token, EndpointContext correlationContext) { // In Leshan we always set context when we send the request, so this should not be needed to implement this. } private byte[] serializeObs(org.eclipse.californium.core.observe.Observation obs) { - return ObservationSerDes.serialize(obs); + return null; +// return observationSerDes.serialize(obs); } - private org.eclipse.californium.core.observe.Observation deserializeObs(byte[] data) { - return ObservationSerDes.deserialize(data); + private Observation deserializeObs(byte[] data) { +// return observationSerDes.deserialize(data); + return null; } - private Observation build(org.eclipse.californium.core.observe.Observation cfObs) { + private Observation build(Observation cfObs) { if (cfObs == null) return null; - - return ObserveUtil.createLwM2mObservation(cfObs.getRequest()); +// String serializedObservation = observationSerDes.serialize(cfObs); +// return ObserveUtil.createLwM2mObservation(cfObs, serializedObservation); + return null; } /* *************** Expiration handling **************** */ @@ -787,7 +813,6 @@ public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationSto expirationListener = listener; } - @Override public void setExecutor(ScheduledExecutorService executor) { // TODO should we reuse californium executor ? } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java index 87e92c8cca..daadc893bf 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.server.store; import org.eclipse.leshan.core.SecurityMode; +import org.eclipse.leshan.core.peer.OscoreIdentity; import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; import org.eclipse.leshan.server.security.SecurityInfo; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -86,6 +87,11 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore { } } + @Override + public SecurityInfo getByOscoreIdentity(OscoreIdentity oscoreIdentity) { + return null; + } + @Override public void put(TbLwM2MSecurityInfo tbSecurityInfo) throws NonUniqueSecurityInfoException { SecurityInfo info = tbSecurityInfo.getSecurityInfo(); @@ -94,21 +100,21 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore { try (var connection = connectionFactory.getConnection()) { lock = redisLock.obtain(tbSecurityInfo.getEndpoint()); lock.lock(); - if (info != null && info.getIdentity() != null) { - byte[] oldEndpointBytes = connection.hGet(PSKID_SEC.getBytes(), info.getIdentity().getBytes()); + if (info != null && info.getPskIdentity() != null) { + byte[] oldEndpointBytes = connection.hGet(PSKID_SEC.getBytes(), info.getPskIdentity().getBytes()); if (oldEndpointBytes != null) { String oldEndpoint = new String(oldEndpointBytes); if (!oldEndpoint.equals(info.getEndpoint())) { - throw new NonUniqueSecurityInfoException("PSK Identity " + info.getIdentity() + " is already used"); + throw new NonUniqueSecurityInfoException("PSK Identity " + info.getPskIdentity() + " is already used"); } - connection.hSet(PSKID_SEC.getBytes(), info.getIdentity().getBytes(), info.getEndpoint().getBytes()); + connection.hSet(PSKID_SEC.getBytes(), info.getPskIdentity().getBytes(), info.getEndpoint().getBytes()); } } byte[] previousData = connection.getSet((SEC_EP + tbSecurityInfo.getEndpoint()).getBytes(), tbSecurityInfoSerialized); if (previousData != null && info != null) { - String previousIdentity = ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(previousData)).getSecurityInfo().getIdentity(); - if (previousIdentity != null && !previousIdentity.equals(info.getIdentity())) { + String previousIdentity = ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(previousData)).getSecurityInfo().getPskIdentity(); + if (previousIdentity != null && !previousIdentity.equals(info.getPskIdentity())) { connection.hDel(PSKID_SEC.getBytes(), previousIdentity.getBytes()); } } @@ -147,8 +153,8 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore { byte[] data = connection.get((SEC_EP + endpoint).getBytes()); if (data != null && data.length > 0) { SecurityInfo info = ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(data)).getSecurityInfo(); - if (info != null && info.getIdentity() != null) { - connection.hDel(PSKID_SEC.getBytes(), info.getIdentity().getBytes()); + if (info != null && info.getPskIdentity() != null) { + connection.hDel(PSKID_SEC.getBytes(), info.getPskIdentity().getBytes()); } connection.del((SEC_EP + endpoint).getBytes()); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java index 5d7e339584..0cdc75f17e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.server.store; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.SecurityMode; +import org.eclipse.leshan.core.peer.OscoreIdentity; import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; import org.eclipse.leshan.server.security.SecurityInfo; import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator; @@ -60,7 +61,7 @@ public class TbLwM2mSecurityStore implements TbMainSecurityStore { if (securityInfo == null) { securityInfo = fetchAndPutSecurityInfo(endpoint); } else if (securityInfo.usePSK() && securityInfo.getEndpoint().equals(SecurityMode.NO_SEC.toString()) - && securityInfo.getIdentity().equals(SecurityMode.NO_SEC.toString()) + && securityInfo.getPskIdentity().equals(SecurityMode.NO_SEC.toString()) && Arrays.equals(SecurityMode.NO_SEC.toString().getBytes(), securityInfo.getPreSharedKey())) { return null; } @@ -81,6 +82,11 @@ public class TbLwM2mSecurityStore implements TbMainSecurityStore { return securityInfo; } + @Override + public SecurityInfo getByOscoreIdentity(OscoreIdentity oscoreIdentity) { + return null; + } + public SecurityInfo fetchAndPutSecurityInfo(String credentialsId) { TbLwM2MSecurityInfo securityInfo = validator.getEndpointSecurityInfoByCredentialsId(credentialsId, CLIENT); doPut(securityInfo); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java index f74630fffc..9aaebaa8bc 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java @@ -16,8 +16,8 @@ package org.thingsboard.server.transport.lwm2m.server.store; import lombok.RequiredArgsConstructor; -import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; -import org.eclipse.leshan.server.californium.registration.InMemoryRegistrationStore; +import org.eclipse.leshan.server.registration.InMemoryRegistrationStore; +import org.eclipse.leshan.server.registration.RegistrationStore; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.stereotype.Component; @@ -38,7 +38,7 @@ public class TbLwM2mStoreFactory { private final LwM2mCredentialsSecurityInfoValidator validator; @Bean - private CaliforniumRegistrationStore registrationStore() { + private RegistrationStore registrationStore() { return redisConfiguration.isPresent() ? new TbLwM2mRedisRegistrationStore(getConnectionFactory()) : new InMemoryRegistrationStore(config.getCleanPeriodInSec()); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java index 88663bae71..1309e9ae2c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java @@ -15,9 +15,8 @@ */ package org.thingsboard.server.transport.lwm2m.server.store.util; -import com.eclipsesource.json.Json; -import com.eclipsesource.json.JsonObject; -import com.eclipsesource.json.JsonValue; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.gson.JsonObject; import com.google.protobuf.util.JsonFormat; import lombok.SneakyThrows; import org.eclipse.leshan.core.model.ResourceModel; @@ -26,113 +25,107 @@ import org.eclipse.leshan.core.node.LwM2mNodeException; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.node.ObjectLink; -import org.eclipse.leshan.core.util.datatype.ULong; import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes; -import org.thingsboard.server.common.data.device.data.PowerMode; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; -import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue; import java.lang.reflect.Field; import java.util.Base64; import java.util.Date; import java.util.HashMap; import java.util.Map; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicLong; public class LwM2MClientSerDes { public static final String VALUE = "value"; @SneakyThrows public static byte[] serialize(LwM2mClient client) { - JsonObject o = Json.object(); - o.add("nodeId", client.getNodeId()); - o.add("endpoint", client.getEndpoint()); + JsonObject o = new JsonObject(); + o.addProperty("nodeId", "client.getNodeId()"); + o.addProperty("endpoint", client.getEndpoint()); - JsonObject resources = Json.object(); + JsonObject resources = new JsonObject(); client.getResources().forEach((k, v) -> { - JsonObject resourceValue = Json.object(); + JsonObject resourceValue = new JsonObject(); resourceValue.add("lwM2mResource", serialize(v.getLwM2mResource())); resourceValue.add("resourceModel", serialize(v.getResourceModel())); resources.add(k, resourceValue); }); o.add("resources", resources); - JsonObject sharedAttributes = Json.object(); + JsonObject sharedAttributes = new JsonObject(); for (Map.Entry entry : client.getSharedAttributes().entrySet()) { - sharedAttributes.add(entry.getKey(), JsonFormat.printer().print(entry.getValue())); + sharedAttributes.addProperty(entry.getKey(), JsonFormat.printer().print(entry.getValue())); } o.add("sharedAttributes", sharedAttributes); - JsonObject keyTsLatestMap = Json.object(); + JsonObject keyTsLatestMap = new JsonObject(); client.getKeyTsLatestMap().forEach((k, v) -> { - keyTsLatestMap.add(k, v.get()); + keyTsLatestMap.addProperty(k, v.get()); }); o.add("keyTsLatestMap", keyTsLatestMap); - o.add("state", client.getState().toString()); + o.addProperty("state", client.getState().toString()); if (client.getSession() != null) { - o.add("session", JsonFormat.printer().print(client.getSession())); + o.addProperty("session", JsonFormat.printer().print(client.getSession())); } if (client.getTenantId() != null) { - o.add("tenantId", client.getTenantId().toString()); + o.addProperty("tenantId", client.getTenantId().toString()); } if (client.getDeviceId() != null) { - o.add("deviceId", client.getDeviceId().toString()); + o.addProperty("deviceId", client.getDeviceId().toString()); } if (client.getProfileId() != null) { - o.add("profileId", client.getProfileId().toString()); + o.addProperty("profileId", client.getProfileId().toString()); } if (client.getPowerMode() != null) { - o.add("powerMode", client.getPowerMode().toString()); + o.addProperty("powerMode", client.getPowerMode().toString()); } if (client.getEdrxCycle() != null) { - o.add("edrxCycle", client.getEdrxCycle()); + o.addProperty("edrxCycle", client.getEdrxCycle()); } if (client.getPsmActivityTimer() != null) { - o.add("psmActivityTimer", client.getPsmActivityTimer()); + o.addProperty("psmActivityTimer", client.getPsmActivityTimer()); } if (client.getPagingTransmissionWindow() != null) { - o.add("pagingTransmissionWindow", client.getPagingTransmissionWindow()); + o.addProperty("pagingTransmissionWindow", client.getPagingTransmissionWindow()); } if (client.getRegistration() != null) { - o.add("registration", RegistrationSerDes.jSerialize(client.getRegistration())); + RegistrationSerDes regDez = new RegistrationSerDes(); + o.addProperty("registration", regDez.jSerialize(client.getRegistration()).toString()); } - o.add("asleep", client.isAsleep()); - o.add("lastUplinkTime", client.getLastUplinkTime()); + o.addProperty("asleep", client.isAsleep()); + o.addProperty("lastUplinkTime", client.getLastUplinkTime()); Field firstEdrxDownlink = LwM2mClient.class.getDeclaredField("firstEdrxDownlink"); firstEdrxDownlink.setAccessible(true); - o.add("firstEdrxDownlink", (boolean) firstEdrxDownlink.get(client)); - o.add("retryAttempts", client.getRetryAttempts().get()); + o.addProperty("firstEdrxDownlink", (boolean) firstEdrxDownlink.get(client)); + o.addProperty("retryAttempts", client.getRetryAttempts().get()); if (client.getLastSentRpcId() != null) { - o.add("lastSentRpcId", client.getLastSentRpcId().toString()); + o.addProperty("lastSentRpcId", client.getLastSentRpcId().toString()); } return o.toString().getBytes(); } private static JsonObject serialize(LwM2mResource resource) { - JsonObject o = Json.object(); - o.add("id", resource.getId()); - o.add("type", resource.getType().toString()); + JsonObject o = new JsonObject(); + o.addProperty("id", resource.getId()); + o.addProperty("type", resource.getType().toString()); if (resource.isMultiInstances()) { - o.add("multiInstances", true); - JsonObject instances = Json.object(); + o.addProperty("multiInstances", true); + JsonObject instances = new JsonObject(); resource.getInstances().forEach((id, in) -> { - JsonObject instance = Json.object(); - instance.add("id", in.getId()); + JsonObject instance = new JsonObject(); + instance.addProperty("id", in.getId()); addValue(instance, in.getType(), in.getValue()); instances.add(id.toString(), instance); }); o.add("instances", instances); } else { - o.add("multiInstances", false); + o.addProperty("multiInstances", false); addValue(o, resource.getType(), resource.getValue()); } @@ -140,95 +133,95 @@ public class LwM2MClientSerDes { } private static LwM2mResource parseLwM2mResource(JsonObject o) { - boolean multiInstances = o.get("multiInstances").asBoolean(); - int id = o.get("id").asInt(); - ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").asString()); + boolean multiInstances = o.get("multiInstances").getAsBoolean(); + int id = o.get("id").getAsInt(); + ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").getAsString()); if (multiInstances) { Map instances = new HashMap<>(); - o.get("instances").asObject().forEach(entry -> { - instances.put(Integer.valueOf(entry.getName()), parseValue(type, entry.getValue())); + o.get("instances").getAsJsonArray().forEach(entry -> { +// instances.put(Integer.valueOf(entry.getAsJsonObject().), parseValue(type, entry.getValue())); }); return LwM2mMultipleResource.newResource(id, instances, type); } else { - return LwM2mSingleResource.newResource(id, parseValue(type, o.get(VALUE))); + return LwM2mSingleResource.newResource(id, parseValue(type, (JsonValue) o.get(VALUE))); } } private static Object parseValue(ResourceModel.Type type, JsonValue value) { switch (type) { case INTEGER: - return value.asLong(); - case FLOAT: - return value.asDouble(); - case BOOLEAN: - return value.asBoolean(); - case OPAQUE: - return Base64.getDecoder().decode(value.asString()); - case STRING: - return value.asString(); - case TIME: - return new Date(value.asLong()); - case OBJLNK: - return ObjectLink.decodeFromString(value.asString()); - case UNSIGNED_INTEGER: - return ULong.valueOf(value.asString()); + return value.value(); +// case FLOAT: +// return value.value(); +// case BOOLEAN: +// return value.asBoolean(); +// case OPAQUE: +// return Base64.getDecoder().decode(value.asString()); +// case STRING: +// return value.asString(); +// case TIME: +// return new Date(value.asLong()); +// case OBJLNK: +// return ObjectLink.decodeFromString(value.asString()); +// case UNSIGNED_INTEGER: +// return ULong.valueOf(value.asString()); default: throw new LwM2mNodeException(String.format("Type %s is not supported", type.name())); } } private static JsonObject serialize(ResourceModel resourceModel) { - JsonObject o = Json.object(); - o.add("id", resourceModel.id); - o.add("name", resourceModel.name); - o.add("operations", resourceModel.operations.toString()); - o.add("multiple", resourceModel.multiple); - o.add("mandatory", resourceModel.mandatory); - o.add("type", resourceModel.type.toString()); - o.add("rangeEnumeration", resourceModel.rangeEnumeration); - o.add("units", resourceModel.units); - o.add("description", resourceModel.description); + JsonObject o = new JsonObject(); + o.addProperty("id", resourceModel.id); + o.addProperty("name", resourceModel.name); + o.addProperty("operations", resourceModel.operations.toString()); + o.addProperty("multiple", resourceModel.multiple); + o.addProperty("mandatory", resourceModel.mandatory); + o.addProperty("type", resourceModel.type.toString()); + o.addProperty("rangeEnumeration", resourceModel.rangeEnumeration); + o.addProperty("units", resourceModel.units); + o.addProperty("description", resourceModel.description); return o; } private static ResourceModel parseResourceModel(JsonObject o) { - Integer id = o.get("id").asInt(); - String name = o.get("name").asString(); - ResourceModel.Operations operations = ResourceModel.Operations.valueOf(o.get("operations").asString()); - Boolean multiple = o.get("multiple").asBoolean(); - Boolean mandatory = o.get("mandatory").asBoolean(); - ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").asString()); - String rangeEnumeration = o.get("rangeEnumeration").asString(); - String units = o.get("units").asString(); - String description = o.get("description").asString(); + Integer id = o.get("id").getAsInt(); + String name = o.get("name").getAsString(); + ResourceModel.Operations operations = ResourceModel.Operations.valueOf(o.get("operations").getAsString()); + Boolean multiple = o.get("multiple").getAsBoolean(); + Boolean mandatory = o.get("mandatory").getAsBoolean(); + ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").getAsString()); + String rangeEnumeration = o.get("rangeEnumeration").getAsString(); + String units = o.get("units").getAsString(); + String description = o.get("description").getAsString(); return new ResourceModel(id, name, operations, multiple, mandatory, type, rangeEnumeration, units, description); } private static void addValue(JsonObject o, ResourceModel.Type type, Object value) { switch (type) { case INTEGER: - o.add(VALUE, (Long) value); + o.addProperty(VALUE, (Long) value); break; case FLOAT: - o.add(VALUE, (Double) value); + o.addProperty(VALUE, (Double) value); break; case BOOLEAN: - o.add(VALUE, (Boolean) value); + o.addProperty(VALUE, (Boolean) value); break; case OPAQUE: - o.add(VALUE, Base64.getEncoder().encodeToString((byte[]) value)); + o.addProperty(VALUE, Base64.getEncoder().encodeToString((byte[]) value)); break; case STRING: - o.add(VALUE, (String) value); + o.addProperty(VALUE, (String) value); break; case TIME: - o.add(VALUE, ((Date) value).getTime()); + o.addProperty(VALUE, ((Date) value).getTime()); break; case OBJLNK: - o.add(VALUE, ((ObjectLink) value).encodeToString()); + o.addProperty(VALUE, ((ObjectLink) value).encodeToString()); break; case UNSIGNED_INTEGER: - o.add(VALUE, value.toString()); + o.addProperty(VALUE, value.toString()); break; default: throw new LwM2mNodeException(String.format("Type %s is not supported", type.name())); @@ -237,111 +230,111 @@ public class LwM2MClientSerDes { @SneakyThrows public static LwM2mClient deserialize(byte[] data) { - JsonObject o = Json.parse(new String(data)).asObject(); - LwM2mClient lwM2mClient = new LwM2mClient(o.get("nodeId").asString(), o.get("endpoint").asString()); - - o.get("resources").asObject().forEach(entry -> { - JsonObject resource = entry.getValue().asObject(); - LwM2mResource lwM2mResource = parseLwM2mResource(resource.get("lwM2mResource").asObject()); - ResourceModel resourceModel = parseResourceModel(resource.get("resourceModel").asObject()); - ResourceValue resourceValue = new ResourceValue(lwM2mResource, resourceModel); - lwM2mClient.getResources().put(entry.getName(), resourceValue); - }); - - for (JsonObject.Member entry : o.get("sharedAttributes").asObject()) { - TransportProtos.TsKvProto.Builder builder = TransportProtos.TsKvProto.newBuilder(); - JsonFormat.parser().merge(entry.getValue().asString(), builder); - lwM2mClient.getSharedAttributes().put(entry.getName(), builder.build()); - } - - o.get("keyTsLatestMap").asObject().forEach(entry -> { - lwM2mClient.getKeyTsLatestMap().put(entry.getName(), new AtomicLong(entry.getValue().asLong())); - }); - - lwM2mClient.setState(LwM2MClientState.valueOf(o.get("state").asString())); - - Class lwM2mClientClass = LwM2mClient.class; - - JsonValue session = o.get("session"); - if (session != null) { - TransportProtos.SessionInfoProto.Builder builder = TransportProtos.SessionInfoProto.newBuilder(); - JsonFormat.parser().merge(session.asString(), builder); - - Field sessionField = lwM2mClientClass.getDeclaredField("session"); - sessionField.setAccessible(true); - sessionField.set(lwM2mClient, builder.build()); - } - - JsonValue tenantId = o.get("tenantId"); - if (tenantId != null) { - Field tenantIdField = lwM2mClientClass.getDeclaredField("tenantId"); - tenantIdField.setAccessible(true); - tenantIdField.set(lwM2mClient, new TenantId(UUID.fromString(tenantId.asString()))); - } - - JsonValue deviceId = o.get("deviceId"); - if (tenantId != null) { - Field deviceIdField = lwM2mClientClass.getDeclaredField("deviceId"); - deviceIdField.setAccessible(true); - deviceIdField.set(lwM2mClient, UUID.fromString(deviceId.asString())); - } - - JsonValue profileId = o.get("profileId"); - if (tenantId != null) { - Field profileIdField = lwM2mClientClass.getDeclaredField("profileId"); - profileIdField.setAccessible(true); - profileIdField.set(lwM2mClient, UUID.fromString(profileId.asString())); - } - - JsonValue powerMode = o.get("powerMode"); - if (powerMode != null) { - Field powerModeField = lwM2mClientClass.getDeclaredField("powerMode"); - powerModeField.setAccessible(true); - powerModeField.set(lwM2mClient, PowerMode.valueOf(powerMode.asString())); - } - - JsonValue edrxCycle = o.get("edrxCycle"); - if (edrxCycle != null) { - Field edrxCycleField = lwM2mClientClass.getDeclaredField("edrxCycle"); - edrxCycleField.setAccessible(true); - edrxCycleField.set(lwM2mClient, edrxCycle.asLong()); - } - - JsonValue psmActivityTimer = o.get("psmActivityTimer"); - if (psmActivityTimer != null) { - Field psmActivityTimerField = lwM2mClientClass.getDeclaredField("psmActivityTimer"); - psmActivityTimerField.setAccessible(true); - psmActivityTimerField.set(lwM2mClient, psmActivityTimer.asLong()); - } - - JsonValue pagingTransmissionWindow = o.get("pagingTransmissionWindow"); - if (pagingTransmissionWindow != null) { - Field pagingTransmissionWindowField = lwM2mClientClass.getDeclaredField("pagingTransmissionWindow"); - pagingTransmissionWindowField.setAccessible(true); - pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.asLong()); - } - - JsonValue registration = o.get("registration"); - if (registration != null) { - lwM2mClient.setRegistration(RegistrationSerDes.deserialize(registration.asObject())); - } - - lwM2mClient.setAsleep(o.get("asleep").asBoolean()); - - Field lastUplinkTimeField = lwM2mClientClass.getDeclaredField("lastUplinkTime"); - lastUplinkTimeField.setAccessible(true); - lastUplinkTimeField.setLong(lwM2mClient, o.get("lastUplinkTime").asLong()); - - Field firstEdrxDownlinkField = lwM2mClientClass.getDeclaredField("firstEdrxDownlink"); - firstEdrxDownlinkField.setAccessible(true); - firstEdrxDownlinkField.setBoolean(lwM2mClient, o.get("firstEdrxDownlink").asBoolean()); - - lwM2mClient.getRetryAttempts().set(o.get("retryAttempts").asInt()); - - JsonValue lastSentRpcId = o.get("lastSentRpcId"); - if (lastSentRpcId != null) { - lwM2mClient.setLastSentRpcId(UUID.fromString(lastSentRpcId.asString())); - } +// JsonObject o = new JsonObject(new String(data))); +// LwM2mClient lwM2mClient = new LwM2mClient(o.get("nodeId").getAsString(), o.get("endpoint").getAsString()); + LwM2mClient lwM2mClient = new LwM2mClient("nodeId", "endpoint"); +// o.get("resources").getAsJsonObject().forEach(entry -> { +// JsonObject resource = entry.getValue().asObject(); +// LwM2mResource lwM2mResource = parseLwM2mResource(resource.get("lwM2mResource").getAsJsonObject()); +// ResourceModel resourceModel = parseResourceModel(resource.get("resourceModel").asObject()); +// ResourceValue resourceValue = new ResourceValue(lwM2mResource, resourceModel); +// lwM2mClient.getResources().put(entry.getName(), resourceValue); +// }); +// +// for (JsonObject.Member entry : o.get("sharedAttributes").asObject()) { +// TransportProtos.TsKvProto.Builder builder = TransportProtos.TsKvProto.newBuilder(); +// JsonFormat.parser().merge(entry.getValue().getAsString(), builder); +// lwM2mClient.getSharedAttributes().put(entry.getName(), builder.build()); +// } +// +// o.get("keyTsLatestMap").asObject().forEach(entry -> { +// lwM2mClient.getKeyTsLatestMap().put(entry.getName(), new AtomicLong(entry.getValue().asLong())); +// }); +// +// lwM2mClient.setState(LwM2MClientState.valueOf(o.get("state").getAsString())); +// +// Class lwM2mClientClass = LwM2mClient.class; +// +// JsonValue session = o.get("session"); +// if (session != null) { +// TransportProtos.SessionInfoProto.Builder builder = TransportProtos.SessionInfoProto.newBuilder(); +// JsonFormat.parser().merge(session.asString(), builder); +// +// Field sessionField = lwM2mClientClass.getDeclaredField("session"); +// sessionField.setAccessible(true); +// sessionField.set(lwM2mClient, builder.build()); +// } +// +// JsonValue tenantId = o.get("tenantId"); +// if (tenantId != null) { +// Field tenantIdField = lwM2mClientClass.getDeclaredField("tenantId"); +// tenantIdField.setAccessible(true); +// tenantIdField.set(lwM2mClient, new TenantId(UUID.fromString(tenantId.asString()))); +// } +// +// JsonValue deviceId = o.get("deviceId"); +// if (tenantId != null) { +// Field deviceIdField = lwM2mClientClass.getDeclaredField("deviceId"); +// deviceIdField.setAccessible(true); +// deviceIdField.set(lwM2mClient, UUID.fromString(deviceId.asString())); +// } +// +// JsonValue profileId = o.get("profileId"); +// if (tenantId != null) { +// Field profileIdField = lwM2mClientClass.getDeclaredField("profileId"); +// profileIdField.setAccessible(true); +// profileIdField.set(lwM2mClient, UUID.fromString(profileId.asString())); +// } +// +// JsonValue powerMode = o.get("powerMode"); +// if (powerMode != null) { +// Field powerModeField = lwM2mClientClass.getDeclaredField("powerMode"); +// powerModeField.setAccessible(true); +// powerModeField.set(lwM2mClient, PowerMode.valueOf(powerMode.asString())); +// } +// +// JsonValue edrxCycle = o.get("edrxCycle"); +// if (edrxCycle != null) { +// Field edrxCycleField = lwM2mClientClass.getDeclaredField("edrxCycle"); +// edrxCycleField.setAccessible(true); +// edrxCycleField.set(lwM2mClient, edrxCycle.asLong()); +// } +// +// JsonValue psmActivityTimer = o.get("psmActivityTimer"); +// if (psmActivityTimer != null) { +// Field psmActivityTimerField = lwM2mClientClass.getDeclaredField("psmActivityTimer"); +// psmActivityTimerField.setAccessible(true); +// psmActivityTimerField.set(lwM2mClient, psmActivityTimer.asLong()); +// } +// +// JsonValue pagingTransmissionWindow = o.get("pagingTransmissionWindow"); +// if (pagingTransmissionWindow != null) { +// Field pagingTransmissionWindowField = lwM2mClientClass.getDeclaredField("pagingTransmissionWindow"); +// pagingTransmissionWindowField.setAccessible(true); +// pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.asLong()); +// } +// +// JsonValue registration = o.get("registration"); +// if (registration != null) { +// lwM2mClient.setRegistration(RegistrationSerDes.deserialize(registration.asObject())); +// } +// +// lwM2mClient.setAsleep(o.get("asleep").getAsBoolean()); +// +// Field lastUplinkTimeField = lwM2mClientClass.getDeclaredField("lastUplinkTime"); +// lastUplinkTimeField.setAccessible(true); +// lastUplinkTimeField.setLong(lwM2mClient, o.get("lastUplinkTime").asLong()); +// +// Field firstEdrxDownlinkField = lwM2mClientClass.getDeclaredField("firstEdrxDownlink"); +// firstEdrxDownlinkField.setAccessible(true); +// firstEdrxDownlinkField.setBoolean(lwM2mClient, o.get("firstEdrxDownlink").getAsBoolean()); +// +// lwM2mClient.getRetryAttempts().set(o.get("retryAttempts").asInt()); +// +// JsonValue lastSentRpcId = o.get("lastSentRpcId"); +// if (lastSentRpcId != null) { +// lwM2mClient.setLastSentRpcId(UUID.fromString(lastSentRpcId.asString())); +// } return lwM2mClient; } 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 4369f5f532..84e1bfa53d 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 @@ -356,7 +356,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl */ @Override public void onUpdateValueWithSendRequest(Registration registration, SendRequest sendRequest) { - for(var entry : sendRequest.getNodes().entrySet()) { + for(var entry : sendRequest.getTimestampedNodes().getNodes().entrySet()) { LwM2mPath path = entry.getKey(); LwM2mNode node = entry.getValue(); LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); @@ -777,7 +777,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl this.updateResourcesValue(client, (LwM2mResource) v, k.toString(), Mode.REPLACE, code); } else { LwM2mResourceInstance resourceInstance = (LwM2mResourceInstance) v; - LwM2mMultipleResource multipleResource = new LwM2mMultipleResource(v.getId(), resourceInstance.getType(), resourceInstance); + LwM2mMultipleResource multipleResource = new LwM2mMultipleResource(((LwM2mResourceInstance) v).getId(), resourceInstance.getType(), resourceInstance); this.updateResourcesValue(client, multipleResource, k.toString(), Mode.REPLACE, code); } }); 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 527a84df22..201d41ce61 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 @@ -15,11 +15,8 @@ */ package org.thingsboard.server.transport.lwm2m.utils; -import com.fasterxml.jackson.core.type.TypeReference; import com.google.gson.JsonElement; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.attributes.Attribute; -import org.eclipse.leshan.core.attributes.AttributeSet; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.model.ObjectLoader; import org.eclipse.leshan.core.model.ObjectModel; @@ -30,8 +27,6 @@ import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.node.codec.CodecException; -import org.eclipse.leshan.core.request.SimpleDownlinkRequest; -import org.eclipse.leshan.core.request.WriteAttributesRequest; import org.eclipse.leshan.core.util.Hex; import org.eclipse.leshan.server.registration.Registration; import org.thingsboard.common.util.JacksonUtil; @@ -52,9 +47,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.LwM2mUplinkMsgHandler; -import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; @@ -62,13 +55,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import static org.eclipse.leshan.core.attributes.Attribute.DIMENSION; -import static org.eclipse.leshan.core.attributes.Attribute.GREATER_THAN; -import static org.eclipse.leshan.core.attributes.Attribute.LESSER_THAN; -import static org.eclipse.leshan.core.attributes.Attribute.MAXIMUM_PERIOD; -import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD; -import static org.eclipse.leshan.core.attributes.Attribute.OBJECT_VERSION; -import static org.eclipse.leshan.core.attributes.Attribute.STEP; import static org.eclipse.leshan.core.model.ResourceModel.Type.BOOLEAN; import static org.eclipse.leshan.core.model.ResourceModel.Type.FLOAT; import static org.eclipse.leshan.core.model.ResourceModel.Type.INTEGER; @@ -83,6 +69,7 @@ import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaU import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SW_RESULT_ID; import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SW_STATE_ID; + @Slf4j public class LwM2MTransportUtil { @@ -235,7 +222,7 @@ public class LwM2MTransportUtil { } public static String convertObjectIdToVersionedId(String path, Registration registration) { - String ver = registration.getSupportedObject().get(new LwM2mPath(path).getObjectId()); + String ver = String.valueOf(registration.getSupportedObject().get(new LwM2mPath(path).getObjectId())); ver = ver != null ? ver : TbLwM2mVersion.VERSION_1_0.getVersion().toString(); try { String[] keyArray = path.split(LWM2M_SEPARATOR_PATH); @@ -289,28 +276,28 @@ public class LwM2MTransportUtil { * Attribute pmax = new Attribute(MAXIMUM_PERIOD, "60"); * Attribute [] attrs = {gt, st}; */ - 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, LwM2mUplinkMsgHandler serviceImpl, String target) { - List attributeLists = new ArrayList<>(); - Map map = JacksonUtil.convertValue(params, new TypeReference<>() { - }); - map.forEach((k, v) -> { - if (StringUtils.trimToNull(v.toString()) != null) { - Object attrValue = convertWriteAttributes(k, v, serviceImpl, target); - if (attrValue != null) { - Attribute attribute = createAttribute(k, attrValue); - if (attribute != null) { - attributeLists.add(new Attribute(k, attrValue)); - } - } - } - }); - return attributeLists.toArray(Attribute[]::new); - } +// 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, LwM2mUplinkMsgHandler serviceImpl, String target) { +// List attributeLists = new ArrayList<>(); +// Map map = JacksonUtil.convertValue(params, new TypeReference<>() { +// }); +// map.forEach((k, v) -> { +// if (StringUtils.trimToNull(v.toString()) != null) { +// Object attrValue = convertWriteAttributes(k, v, serviceImpl, target); +// if (attrValue != null) { +// Attribute attribute = createAttribute(k, attrValue); +// if (attribute != null) { +// attributeLists.add(new Attribute(k, attrValue)); +// } +// } +// } +// }); +// return attributeLists.toArray(Attribute[]::new); +// } /** * "UNSIGNED_INTEGER": // Number -> Integer Example: @@ -360,46 +347,50 @@ public class LwM2MTransportUtil { public static Map convertMultiResourceValuesFromJson(JsonElement newValProto, ResourceModel.Type type, String versionedId) { Map newValues = new HashMap<>(); newValProto.getAsJsonObject().entrySet().forEach((obj) -> { - newValues.put(Integer.valueOf(obj.getKey()), LwM2mValueConverterImpl.getInstance().convertValue(obj.getValue().getAsString(), - STRING, type, new LwM2mPath(fromVersionedIdToObjectId(versionedId)))); + newValues.put(Integer.valueOf(obj.getKey()), convertValueByTypeResource (obj.getValue().getAsString(), type, versionedId)); }); return newValues; } - public static Object convertWriteAttributes(String type, Object value, LwM2mUplinkMsgHandler serviceImpl, String target) { - switch (type) { - /** Integer [0:255]; */ - case DIMENSION: - 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.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), STRING, new LwM2mPath(target)); - /**INTEGER */ - case MINIMUM_PERIOD: - case MAXIMUM_PERIOD: - return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); - /**Float; */ - case GREATER_THAN: - case LESSER_THAN: - case STEP: - if (value.getClass().getSimpleName().equals("String")) { - value = Double.valueOf((String) value); - } - return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), FLOAT, new LwM2mPath(target)); - default: - return null; - } + public static Object convertValueByTypeResource (String value, ResourceModel.Type type, String versionedId) { + return LwM2mValueConverterImpl.getInstance().convertValue(value, + STRING, type, new LwM2mPath(fromVersionedIdToObjectId(versionedId))); } - private static Attribute createAttribute(String key, Object attrValue) { - try { - return new Attribute(key, attrValue); - } catch (Exception e) { - log.error("CreateAttribute, not valid parameter key: [{}], attrValue: [{}], error: [{}]", key, attrValue, e.getMessage()); - return null; - } - } +// public static Object convertWriteAttributes(String type, Object value, LwM2mUplinkMsgHandler serviceImpl, String target) { +// switch (type) { +// /** Integer [0:255]; */ +// case DIMENSION: +// 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.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), STRING, new LwM2mPath(target)); +// /**INTEGER */ +// case MINIMUM_PERIOD: +// case MAXIMUM_PERIOD: +// return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); +// /**Float; */ +// case GREATER_THAN: +// case LESSER_THAN: +// case STEP: +// if (value.getClass().getSimpleName().equals("String")) { +// value = Double.valueOf((String) value); +// } +// return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), FLOAT, new LwM2mPath(target)); +// default: +// return null; +// } +// } + +// private static Attribute createAttribute(String key, Object attrValue) { +// try { +// return new Attribute(key, attrValue); +// } catch (Exception e) { +// log.error("CreateAttribute, not valid parameter key: [{}], attrValue: [{}], error: [{}]", key, attrValue, e.getMessage()); +// return null; +// } +// } /** * @param lwM2MClient - diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java index 89510c6076..9e56d36209 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java @@ -327,10 +327,13 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator 65534) { - throw new DeviceCredentialsValidationException(server + " ShortServerId must not be less than 1 and more than 65534!"); + if (!serverConfig.isBootstrapServerIs() && (serverConfig.getShortServerId() < 1 || serverConfig.getShortServerId() > 65534)) { + throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must not be less than 1 and more than 65534!"); } + if (serverConfig.isBootstrapServerIs() && !(serverConfig.getShortServerId() == null || serverConfig.getShortServerId() ==0)) { + throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be null or '0'!"); + } + String server = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server"; if (!shortServerIds.add(serverConfig.getShortServerId())) { throw new DeviceCredentialsValidationException(server + " \"Short server Id\" value = " + serverConfig.getShortServerId() + ". This value must be a unique value for all servers!"); } diff --git a/pom.xml b/pom.xml index 64ed92bbc6..528ae6353a 100755 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ /usr/share/${pkg.name} 2.1.1 4.0.0 - 2.3.1 + 2.4.0-b180830.0359 4.0.2 3.1.0 3.1.0 @@ -67,13 +67,13 @@ 4.5.13 4.4.14 2.8.1 - 2.13.4 - 2.13.4.2 + 2.15.3 + 2.15.3 1.3.4 4.2.1 2.2.6 - 3.0.0 - 2.0.0-M5 + 3.9.1 + 2.0.0-M14 2.9.0 2.3.30 2.0.1 diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index ef592b50cd..65b7906463 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -25,7 +25,7 @@ export const ATTRIBUTE = 'attribute'; export const TELEMETRY = 'telemetry'; export const KEY_NAME = 'keyName'; export const DEFAULT_ID_SERVER = 123; -export const DEFAULT_ID_BOOTSTRAP = 111; +export const DEFAULT_ID_BOOTSTRAP = 0; export const DEFAULT_LOCAL_HOST_NAME = 'localhost'; export const DEFAULT_PORT_SERVER_NO_SEC = 5685; export const DEFAULT_PORT_BOOTSTRAP_NO_SEC = 5687; From 03560f05577c7c73f708271c98eb2418d2df52e0 Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 26 Dec 2023 17:26:51 +0200 Subject: [PATCH 02/23] lwm2ml: add tests attributes --- ...pcLwm2mIntegrationWriteAttributesTest.java | 37 +++++++++++++-- .../profile/lwm2m/ObjectAttributes.java | 15 ++++++- .../DefaultLwM2mDownlinkMsgHandler.java | 45 +++++++++++++++---- 3 files changed, 83 insertions(+), 14 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java index 6275a657b5..c2d87149de 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java @@ -17,7 +17,6 @@ package org.thingsboard.server.transport.lwm2m.rpc.sql; import com.fasterxml.jackson.databind.node.ObjectNode; import org.eclipse.leshan.core.ResponseCode; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; @@ -30,22 +29,47 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest { - @Ignore /** * WriteAttributes {"id":"/3/0/14","attributes":{"pmax":100, "pmin":10}} * if not implemented: - * {"result":"BAD_REQUEST","error":"Content Format is mandatory"} + * {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"} * if implemented: * {"result":"CHANGED"} */ @Test public void testWriteAttributesResourceWithParametersById_Result_INTERNAL_SERVER_ERROR() throws Exception { String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; + sendRPCReadById(expectedPath); String expectedValue = "{\"pmax\":100, \"pmin\":10}"; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText()); + String expected = "not implemented"; + String actual = rpcActualResult.get("error").asText(); + assertTrue(actual.equals(expected)); + } + @Test + public void testWriteAttributesResourceVerWithParametersById_Result_BAD_REQUEST() throws Exception { + String expectedPath = objectIdVer_3; + String actualResult = sendRPCReadById(expectedPath); + String expectedValue = "{\"ver\":1.3}"; + actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); - String expected = "Content Format is mandatory"; + String expected = "Attribute ver is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request."; + String actual = rpcActualResult.get("error").asText(); + assertTrue(actual.equals(expected)); + } + + @Test + public void testWriteAttributesResourceServerUriWithParametersById_Result_BAD_REQUEST() throws Exception { + String expectedPath = objectInstanceIdVer_1; + String actualResult = sendRPCReadById(expectedPath); + String expectedValue = "{\"uri\":\"coaps://localhost:5690\"}"; + actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); + String expected = "Attribute uri is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request."; String actual = rpcActualResult.get("error").asText(); assertTrue(actual.equals(expected)); } @@ -55,4 +79,9 @@ public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MInte return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); } + private String sendRPCReadById(String path) throws Exception { + String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; + return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java index b195068b62..f22ffe4104 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.device.profile.lwm2m; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; +import org.eclipse.leshan.core.LwM2m; import java.io.Serializable; @@ -25,13 +26,25 @@ import java.io.Serializable; public class ObjectAttributes implements Serializable { private static final long serialVersionUID = 4765123984733721312L; - private Long dim; + private Long ssid; + private String uri; private String ver; + private String lwm2m; private Long pmin; private Long pmax; private Double gt; private Double lt; private Double st; + private Long epmin; + private Long epmax; + + public LwM2m.Version getVer(){ + return ver != null ? new LwM2m.Version(ver) : null; + } + + public LwM2m.LwM2mVersion getLwm2m(){ + return lwm2m != null ? LwM2m.LwM2mVersion.get(lwm2m) : null; + } } 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 e124b8142d..3f2f8c598e 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 @@ -92,10 +92,17 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; +import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.DIMENSION; +import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.ENABLER_VERSION; +import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD; +import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.EVALUATE_MINIMUM_PERIOD; import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.GREATER_THAN; import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.LESSER_THAN; import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.MAXIMUM_PERIOD; import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.MINIMUM_PERIOD; +import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.OBJECT_VERSION; +import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.SERVER_URI; +import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.SHORT_SERVER_ID; import static org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes.STEP; import static org.eclipse.leshan.core.model.ResourceModel.Type.OBJLNK; import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; @@ -281,20 +288,40 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im if (request.getAttributes() == null) { throw new IllegalArgumentException("Attributes to write are not specified!"); } - ObjectAttributes params = request.getAttributes(); - List> attributes = new LinkedList<>(); - addAttribute(attributes, MAXIMUM_PERIOD, params.getPmax()); - addAttribute(attributes, MINIMUM_PERIOD, params.getPmin()); - addAttribute(attributes, GREATER_THAN, params.getGt()); - addAttribute(attributes, LESSER_THAN, params.getLt()); - addAttribute(attributes, STEP, params.getSt()); - LwM2mAttributeSet attributeSet = new LwM2mAttributeSet(attributes); - sendSimpleRequest(client, new WriteAttributesRequest(request.getObjectId(), attributeSet), request.getTimeout(), callback); + sendSimpleRequest(client, new WriteAttributesRequest(request.getObjectId(), getAttributesSet(request.getAttributes())), request.getTimeout(), callback); } catch (InvalidRequestException e) { callback.onValidationError(request.toString(), e.getMessage()); } } + private LwM2mAttributeSet getAttributesSet(ObjectAttributes params) { + List> attributes = new LinkedList<>(); + /** + * Only: AttributeClass.NOTIFICATION -> RW + */ + addAttribute(attributes, MAXIMUM_PERIOD, params.getPmax()); + addAttribute(attributes, MINIMUM_PERIOD, params.getPmin()); + addAttribute(attributes, GREATER_THAN, params.getGt()); + addAttribute(attributes, LESSER_THAN, params.getLt()); + addAttribute(attributes, STEP, params.getSt()); + addAttribute(attributes, EVALUATE_MAXIMUM_PERIOD, params.getEpmax()); + addAttribute(attributes, EVALUATE_MINIMUM_PERIOD, params.getEpmin()); + /** + * Only: AttributeClass.PROPERTIES -> R + */ + addAttribute(attributes, DIMENSION, params.getDim()); // Attachment.RESOURCE + addAttribute(attributes, SHORT_SERVER_ID, params.getSsid()); // Attachment.OBJECT_INSTANCE + addAttribute(attributes, SERVER_URI, params.getUri()); // Attachment.OBJECT_INSTANCE + if (params.getLwm2m() != null) { + addAttribute(attributes, ENABLER_VERSION, params.getLwm2m()); // attachment.ROOT + } + if (params.getVer() != null) { + addAttribute(attributes, OBJECT_VERSION, params.getVer()); // Attachment.OBJECT + } + + return new LwM2mAttributeSet(attributes); + } + @Override public void sendWriteReplaceRequest(LwM2mClient client, TbLwM2MWriteReplaceRequest request, DownlinkRequestCallback callback) { LwM2mPath resultIds = new LwM2mPath(request.getObjectId()); From a97f52c6984b2296088fdf62498dfd165f277eb0 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 28 Dec 2023 15:50:47 +0200 Subject: [PATCH 03/23] lwm2m: comments (why full import?) --- .../server/transport/lwm2m/client/FwLwM2MDevice.java | 7 ++++--- .../lwm2m/client/LwM2mBinaryAppDataContainer.java | 5 +++-- .../server/transport/lwm2m/client/LwM2mLocation.java | 3 ++- .../transport/lwm2m/client/LwM2mTemperatureSensor.java | 5 +++-- .../server/transport/lwm2m/client/Lwm2mServer.java | 7 ++++--- .../server/transport/lwm2m/client/SimpleLwM2MDevice.java | 7 ++++--- .../server/transport/lwm2m/client/SwLwM2MDevice.java | 7 ++++--- 7 files changed, 24 insertions(+), 17 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java index 86124ccb5f..444158b956 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.request.argument.Arguments; @@ -45,7 +46,7 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { private final AtomicInteger updateResult = new AtomicInteger(0); @Override - public ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId) { + public ReadResponse read(LwM2mServer identity, int resourceId) { if (!identity.isSystem()) log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); switch (resourceId) { @@ -65,7 +66,7 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { } @Override - public ExecuteResponse execute(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId, Arguments arguments) { + public ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) { String withArguments = ""; if (!arguments.isEmpty()) withArguments = " with arguments " + arguments; @@ -82,7 +83,7 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { } @Override - public WriteResponse write(org.eclipse.leshan.client.servers.LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { + public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); switch (resourceId) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java index 35866dea0f..5b4dedbba5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; @@ -92,7 +93,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements } @Override - public ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId) { + public ReadResponse read(LwM2mServer identity, int resourceId) { try { switch (resourceId) { case 0: @@ -117,7 +118,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements } @Override - public WriteResponse write(org.eclipse.leshan.client.servers.LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { + public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { log.info("Write on Device resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); switch (resourceId) { case 0: diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java index 4599f7c0ee..8d76498ccf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.response.ReadResponse; @@ -84,7 +85,7 @@ public class LwM2mLocation extends BaseInstanceEnabler implements Destroyable { } @Override - public ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId) { + public ReadResponse read(LwM2mServer identity, int resourceId) { log.info("Read on Location resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); switch (resourceId) { case 0: diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java index ae0297e02b..bd7703dbb3 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.request.argument.Arguments; import org.eclipse.leshan.core.response.ExecuteResponse; @@ -56,7 +57,7 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr } @Override - public synchronized ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId) { + public synchronized ReadResponse read(LwM2mServer identity, int resourceId) { log.info("Read on Temperature resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); switch (resourceId) { case 5601: @@ -73,7 +74,7 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr } @Override - public synchronized ExecuteResponse execute(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId, Arguments arguments) { + public synchronized ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) { log.info("Execute on Temperature resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); switch (resourceId) { case 5605: diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/Lwm2mServer.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/Lwm2mServer.java index 1cddbb17f8..9ab30b3dde 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/Lwm2mServer.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/Lwm2mServer.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel.Type; import org.eclipse.leshan.core.node.LwM2mResource; @@ -70,7 +71,7 @@ public class Lwm2mServer extends BaseInstanceEnabler { } @Override - public ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceid) { + public ReadResponse read(LwM2mServer identity, int resourceid) { if (!identity.isSystem()) LOG.debug("Read on Server resource /{}/{}/{}", getModel().id, getId(), resourceid); @@ -108,7 +109,7 @@ public class Lwm2mServer extends BaseInstanceEnabler { } @Override - public WriteResponse write(org.eclipse.leshan.client.servers.LwM2mServer identity, boolean replace, int resourceid, LwM2mResource value) { + public WriteResponse write(LwM2mServer identity, boolean replace, int resourceid, LwM2mResource value) { if (!identity.isSystem()) log.debug("Write on Server resource /{}/{}/{}", getModel().id, getId(), resourceid); @@ -195,7 +196,7 @@ public class Lwm2mServer extends BaseInstanceEnabler { } } - public ExecuteResponse execute(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceid, Arguments arguments) { + public ExecuteResponse execute(LwM2mServer identity, int resourceid, Arguments arguments) { log.info("Execute on Server resource /{}/{}/{}", getModel().id, getId(), resourceid); if (resourceid == 8) { getLwM2mClient().triggerRegistrationUpdate(identity); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java index 8a32092572..e9bc2c9d48 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; @@ -66,7 +67,7 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl @Override - public ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId) { + public ReadResponse read(LwM2mServer identity, int resourceId) { if (!identity.isSystem()) log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); switch (resourceId) { @@ -108,7 +109,7 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl } @Override - public ExecuteResponse execute(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId, Arguments arguments) { + public ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) { String withArguments = ""; if (!arguments.isEmpty()) withArguments = " with arguments " + arguments; @@ -117,7 +118,7 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl } @Override - public WriteResponse write(org.eclipse.leshan.client.servers.LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { + public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); switch (resourceId) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SwLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SwLwM2MDevice.java index 340f9ffcb2..da6bbcd379 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SwLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SwLwM2MDevice.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.request.argument.Arguments; @@ -45,7 +46,7 @@ public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { private final AtomicInteger updateResult = new AtomicInteger(0); @Override - public ReadResponse read(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId) { + public ReadResponse read(LwM2mServer identity, int resourceId) { if (!identity.isSystem()) log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); switch (resourceId) { @@ -63,7 +64,7 @@ public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { } @Override - public ExecuteResponse execute(org.eclipse.leshan.client.servers.LwM2mServer identity, int resourceId, Arguments arguments) { + public ExecuteResponse execute(LwM2mServer identity, int resourceId, Arguments arguments) { String withArguments = ""; if (!arguments.isEmpty()) withArguments = " with arguments " + arguments; @@ -81,7 +82,7 @@ public class SwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { } @Override - public WriteResponse write(org.eclipse.leshan.client.servers.LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { + public WriteResponse write(LwM2mServer identity, boolean replace, int resourceId, LwM2mResource value) { log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceId); switch (resourceId) { From 992767d7b102e3560e3682e4630a29c5843ede01 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 28 Dec 2023 16:44:31 +0200 Subject: [PATCH 04/23] lwm2m: redis --- .../store/TbLwM2mRedisRegistrationStore.java | 92 +++++++++---------- 1 file changed, 41 insertions(+), 51 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index 40a281728b..fa44c2a93b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -333,12 +333,50 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab @Override public Collection addObservation(String registrationId, Observation observation, boolean addIfAbsent) { - return null; + List removed = new ArrayList<>(); + try (var connection = connectionFactory.getConnection()) { + + // fetch the client ep by registration ID index + byte[] ep = connection.get(toRegIdKey(registrationId)); + if (ep == null) { + return null; + } + + Lock lock = null; + String lockKey = toLockKey(ep); + + try { + lock = redisLock.obtain(lockKey); + lock.lock(); + + // cancel existing observations for the same path and registration id. + for (Observation obs : getObservations(connection, registrationId)) { + //TODO: should be able to use CompositeObservation + if (((SingleObservation)observation).getPath().equals(((SingleObservation)obs).getPath()) + && !observation.getId().equals(obs.getId())) { + removed.add(obs); + unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); + } + } + + } finally { + if (lock != null) { + lock.unlock(); + } + } + } + return removed; } + @Override + public Collection getObservations(String registrationId) { + try (var connection = connectionFactory.getConnection()) { + return getObservations(connection, registrationId); + } + } @Override public Observation getObservation(String registrationId, ObservationIdentifier observationId) { - return null; + return getObservations(registrationId).stream().filter(o -> o.getId()==observationId).findFirst().get(); } @Override @@ -348,7 +386,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab @Override public Observation removeObservation(String registrationId, ObservationIdentifier observationId) { - return null; + return removeObservation(registrationId, observationId.getBytes()); } private Deregistration removeRegistration(RedisConnection connection, String registrationId, boolean removeOnlyIfNotAlive) { @@ -461,42 +499,6 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab * org.eclipse.californium.core.observe.ObservationStore#add method) */ - public Collection addObservation(String registrationId, Observation observation) { - List removed = new ArrayList<>(); - try (var connection = connectionFactory.getConnection()) { - - // fetch the client ep by registration ID index - byte[] ep = connection.get(toRegIdKey(registrationId)); - if (ep == null) { - return null; - } - - Lock lock = null; - String lockKey = toLockKey(ep); - - try { - lock = redisLock.obtain(lockKey); - lock.lock(); - - // cancel existing observations for the same path and registration id. - for (Observation obs : getObservations(connection, registrationId)) { - //TODO: should be able to use CompositeObservation - if (((SingleObservation)observation).getPath().equals(((SingleObservation)obs).getPath()) - && !observation.getId().equals(obs.getId())) { - removed.add(obs); - unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); - } - } - - } finally { - if (lock != null) { - lock.unlock(); - } - } - } - return removed; - } - public Observation removeObservation(String registrationId, byte[] observationId) { try (var connection = connectionFactory.getConnection()) { @@ -529,18 +531,6 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab } } - public Observation getObservation(String registrationId, byte[] observationId) { -// return build(get(new Token(observationId))); - return build(null); - } - - @Override - public Collection getObservations(String registrationId) { - try (var connection = connectionFactory.getConnection()) { - return getObservations(connection, registrationId); - } - } - private Collection getObservations(RedisConnection connection, String registrationId) { Collection result = new ArrayList<>(); for (byte[] token : connection.lRange(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, -1)) { From 8c5210b3871929d5d1567505c936a67e423593f2 Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 2 Jan 2024 19:31:25 +0200 Subject: [PATCH 05/23] lwm2m: add test, 'dim' Attributes implemented --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 6 +- .../transport/lwm2m/Lwm2mTestHelper.java | 1 + .../DefaultLwM2mLinkParserTest.java | 4 +- .../lwm2m/client/LwM2MTestClient.java | 3 +- .../lwm2m/client/SimpleLwM2MDevice.java | 12 +- .../lwm2m/client/TbLwm2mObjectEnabler.java | 642 ++++++++++++++++++ .../lwm2m/client/TbObjectsInitializer.java | 56 ++ .../sql/RpcLwm2mIntegrationDiscoverTest.java | 8 +- ...pcLwm2mIntegrationWriteAttributesTest.java | 50 +- application/src/test/resources/lwm2m/3.xml | 2 +- 10 files changed, 768 insertions(+), 16 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java 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 deb8b8230f..4378c3758e 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 @@ -62,7 +62,6 @@ import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryCmdsWrapper; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; @@ -75,7 +74,6 @@ import java.io.IOException; import java.net.ServerSocket; import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -148,13 +146,13 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte " {\n" + " \"keyName\": {\n" + - " \"/3_1.0/0/9\": \"batteryLevel\"\n" + + " \"/3_1.2/0/9\": \"batteryLevel\"\n" + " },\n" + " \"observe\": [],\n" + " \"attribute\": [\n" + " ],\n" + " \"telemetry\": [\n" + - " \"/3_1.0/0/9\"\n" + + " \"/3_1.2/0/9\"\n" + " ],\n" + " \"attributeLwm2m\": {}\n" + " }"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 9e47eab80b..6eb8dab73f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -34,6 +34,7 @@ public class Lwm2mTestHelper { public static final int RESOURCE_ID_2 = 2; public static final int RESOURCE_ID_3 = 3; public static final int RESOURCE_ID_4 = 4; + public static final int RESOURCE_ID_6 = 6; public static final int RESOURCE_ID_7 = 7; public static final int RESOURCE_ID_8 = 8; public static final int RESOURCE_ID_9 = 9; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java index b5d8fecf10..6f872432ec 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java @@ -34,7 +34,7 @@ public class DefaultLwM2mLinkParserTest { @Test public void check_invalid_values() throws LinkParseException { - // first check it's OK with valid value + // first check it's OK with valid value (3/0/11 - "errorCodes") LwM2mLink[] parsed = parser.parseLwM2mLinkFromCoreLinkFormat(";dim=255".getBytes(), null); assertEquals(new LwM2mPath(3, 0, 11), parsed[0].getPath()); AttributeSet attResult = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.DIMENSION, 255l)); @@ -69,7 +69,7 @@ public class DefaultLwM2mLinkParserTest { // then check an invalid one assertThrowsExactly(LinkParseException.class, () -> { - // dim should be between 0-255 + // pmin should be with value parser.parseLwM2mLinkFromCoreLinkFormat(";pmin".getBytes(), null); }); } 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 6e6fa12cf7..19f32d175f 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 @@ -36,7 +36,6 @@ import org.eclipse.leshan.client.observer.LwM2mClientObserver; import org.eclipse.leshan.client.resource.DummyInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; -import org.eclipse.leshan.client.resource.ObjectsInitializer; import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter; import org.eclipse.leshan.client.send.ManualDataSender; import org.eclipse.leshan.client.servers.LwM2mServer; @@ -133,7 +132,7 @@ public class LwM2MTestClient { models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); } LwM2mModel model = new StaticModel(models); - ObjectsInitializer initializer = new ObjectsInitializer(model); + TbObjectsInitializer initializer = new TbObjectsInitializer(model); if (securityBs != null && security != null) { // SECURITY diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java index e9bc2c9d48..e7e2320b38 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java @@ -47,7 +47,7 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl private static final int min = 5; private static final int max = 50; private static final PrimitiveIterator.OfInt randomIterator = new Random().ints(min,max + 1).iterator(); - private static final List supportedResources = Arrays.asList(0, 1, 2, 3, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21); + private static final List supportedResources = Arrays.asList(0, 1, 2, 3, 6, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21); public SimpleLwM2MDevice() { @@ -79,6 +79,8 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl return ReadResponse.success(resourceId, getSerialNumber()); case 3: return ReadResponse.success(resourceId, getFirmwareVersion()); + case 6: + return ReadResponse.success(resourceId, getAvailablePowerSources(), ResourceModel.Type.INTEGER); case 9: return ReadResponse.success(resourceId, getBatteryLevel()); case 10: @@ -157,6 +159,14 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl return 0; } + private Map getAvailablePowerSources() { + Map availablePowerSources = new HashMap<>(); + availablePowerSources.put(0, 1L); + availablePowerSources.put(1, 2L); + availablePowerSources.put(2, 5L); + return availablePowerSources; + } + private int getBatteryLevel() { return randomIterator.nextInt(); // return 42; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java new file mode 100644 index 0000000000..a0c1333069 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java @@ -0,0 +1,642 @@ +package org.thingsboard.server.transport.lwm2m.client; + +import org.eclipse.leshan.client.LwM2mClient; +import org.eclipse.leshan.client.resource.BaseObjectEnabler; +import org.eclipse.leshan.client.resource.DummyInstanceEnabler; +import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; +import org.eclipse.leshan.client.resource.LwM2mInstanceEnablerFactory; +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.resource.listener.ResourceListener; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.client.servers.ServersInfoExtractor; +import org.eclipse.leshan.client.util.LinkFormatHelper; +import org.eclipse.leshan.core.Destroyable; +import org.eclipse.leshan.core.LwM2mId; +import org.eclipse.leshan.core.Startable; +import org.eclipse.leshan.core.Stoppable; +import org.eclipse.leshan.core.link.lwm2m.LwM2mLink; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mMultipleResource; +import org.eclipse.leshan.core.node.LwM2mObject; +import org.eclipse.leshan.core.node.LwM2mObjectInstance; +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.request.BootstrapDeleteRequest; +import org.eclipse.leshan.core.request.BootstrapReadRequest; +import org.eclipse.leshan.core.request.BootstrapWriteRequest; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.CreateRequest; +import org.eclipse.leshan.core.request.DeleteRequest; +import org.eclipse.leshan.core.request.DiscoverRequest; +import org.eclipse.leshan.core.request.DownlinkRequest; +import org.eclipse.leshan.core.request.ExecuteRequest; +import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.request.ReadRequest; +import org.eclipse.leshan.core.request.WriteAttributesRequest; +import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.request.WriteRequest.Mode; +import org.eclipse.leshan.core.response.BootstrapDeleteResponse; +import org.eclipse.leshan.core.response.BootstrapReadResponse; +import org.eclipse.leshan.core.response.BootstrapWriteResponse; +import org.eclipse.leshan.core.response.CreateResponse; +import org.eclipse.leshan.core.response.DeleteResponse; +import org.eclipse.leshan.core.response.DiscoverResponse; +import org.eclipse.leshan.core.response.ExecuteResponse; +import org.eclipse.leshan.core.response.ObserveResponse; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.core.response.WriteAttributesResponse; +import org.eclipse.leshan.core.response.WriteResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public class TbLwm2mObjectEnabler extends BaseObjectEnabler implements Destroyable, Startable, Stoppable { + + private static Logger LOG = LoggerFactory.getLogger(DummyInstanceEnabler.class); + + protected Map instances; + protected LwM2mInstanceEnablerFactory instanceFactory; + protected ContentFormat defaultContentFormat; + + private LinkFormatHelper tbLinkFormatHelper; + + public TbLwm2mObjectEnabler(int id, ObjectModel objectModel, Map instances, + LwM2mInstanceEnablerFactory instanceFactory, ContentFormat defaultContentFormat) { + super(id, objectModel); + this.instances = new HashMap<>(instances); + this.instanceFactory = instanceFactory; + this.defaultContentFormat = defaultContentFormat; + for (Entry entry : this.instances.entrySet()) { + instances.put(entry.getKey(), entry.getValue()); + listenInstance(entry.getValue(), entry.getKey()); + } + } + + public TbLwm2mObjectEnabler(int id, ObjectModel objectModel) { + super(id, objectModel); + } + + @Override + public synchronized List getAvailableInstanceIds() { + List ids = new ArrayList<>(instances.keySet()); + Collections.sort(ids); + return ids; + } + + @Override + public synchronized List getAvailableResourceIds(int instanceId) { + LwM2mInstanceEnabler instanceEnabler = instances.get(instanceId); + if (instanceEnabler != null) { + return instanceEnabler.getAvailableResourceIds(getObjectModel()); + } else { + return Collections.emptyList(); + } + } + + public synchronized void addInstance(int instanceId, LwM2mInstanceEnabler newInstance) { + instances.put(instanceId, newInstance); + listenInstance(newInstance, instanceId); + fireInstancesAdded(instanceId); + } + + public synchronized LwM2mInstanceEnabler getInstance(int instanceId) { + return instances.get(instanceId); + } + + public synchronized LwM2mInstanceEnabler removeInstance(int instanceId) { + LwM2mInstanceEnabler removedInstance = instances.remove(instanceId); + if (removedInstance != null) { + fireInstancesRemoved(removedInstance.getId()); + } + return removedInstance; + } + + @Override + protected CreateResponse doCreate(LwM2mServer server, CreateRequest request) { + if (!getObjectModel().multiple && instances.size() > 0) { + return CreateResponse.badRequest("an instance already exist for this single instance object"); + } + + if (request.unknownObjectInstanceId()) { + // create instance + LwM2mInstanceEnabler newInstance = createInstance(server, getObjectModel().multiple ? null : 0, + request.getResources()); + + // add new instance to this object + instances.put(newInstance.getId(), newInstance); + listenInstance(newInstance, newInstance.getId()); + fireInstancesAdded(newInstance.getId()); + + return CreateResponse + .success(new LwM2mPath(request.getPath().getObjectId(), newInstance.getId()).toString()); + } else { + List instanceNodes = request.getObjectInstances(); + + // checks single object instances + if (!getObjectModel().multiple) { + if (request.getObjectInstances().size() > 1) { + return CreateResponse.badRequest("can not create several instances on this single instance object"); + } + if (request.getObjectInstances().get(0).getId() != 0) { + return CreateResponse.badRequest("single instance object must use 0 as ID"); + } + } + // ensure instance does not already exists + for (LwM2mObjectInstance instance : instanceNodes) { + if (instances.containsKey(instance.getId())) { + return CreateResponse.badRequest(String.format("instance %d already exists", instance.getId())); + } + } + + // create the new instances + int[] instanceIds = new int[request.getObjectInstances().size()]; + int i = 0; + for (LwM2mObjectInstance instance : request.getObjectInstances()) { + // create instance + LwM2mInstanceEnabler newInstance = createInstance(server, instance.getId(), + instance.getResources().values()); + + // add new instance to this object + instances.put(newInstance.getId(), newInstance); + listenInstance(newInstance, newInstance.getId()); + + // store instance ids + instanceIds[i] = newInstance.getId(); + i++; + } + fireInstancesAdded(instanceIds); + return CreateResponse.success(); + } + } + + protected LwM2mInstanceEnabler createInstance(LwM2mServer server, Integer instanceId, + Collection resources) { + // create the new instance + LwM2mInstanceEnabler newInstance = instanceFactory.create(getObjectModel(), instanceId, instances.keySet()); + newInstance.setLwM2mClient(getLwm2mClient()); + + // add/write resource + for (LwM2mResource resource : resources) { + newInstance.write(server, true, resource.getId(), resource); + } + + return newInstance; + } + + @Override + protected ReadResponse doRead(LwM2mServer server, ReadRequest request) { + LwM2mPath path = request.getPath(); + + // Manage Object case + if (path.isObject()) { + List lwM2mObjectInstances = new ArrayList<>(); + for (LwM2mInstanceEnabler instance : instances.values()) { + ReadResponse response = instance.read(server); + if (response.isSuccess()) { + lwM2mObjectInstances.add((LwM2mObjectInstance) response.getContent()); + } + } + return ReadResponse.success(new LwM2mObject(getId(), lwM2mObjectInstances)); + } + + // Manage Instance case + LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); + if (instance == null) + return ReadResponse.notFound(); + + if (path.getResourceId() == null) { + return instance.read(server); + } + + // Manage Resource case + if (path.getResourceInstanceId() == null) { + return instance.read(server, path.getResourceId()); + } + + // Manage Resource Instance case + return instance.read(server, path.getResourceId(), path.getResourceInstanceId()); + } + + @Override + protected BootstrapReadResponse doRead(LwM2mServer server, BootstrapReadRequest request) { + // Basic implementation we delegate to classic Read Request + ReadResponse response = doRead(server, + new ReadRequest(request.getContentFormat(), request.getPath(), request.getCoapRequest())); + return new BootstrapReadResponse(response.getCode(), response.getContent(), response.getErrorMessage()); + } + + @Override + protected ObserveResponse doObserve(final LwM2mServer server, final ObserveRequest request) { + final LwM2mPath path = request.getPath(); + + // Manage Object case + if (path.isObject()) { + List lwM2mObjectInstances = new ArrayList<>(); + for (LwM2mInstanceEnabler instance : instances.values()) { + ReadResponse response = instance.observe(server); + if (response.isSuccess()) { + lwM2mObjectInstances.add((LwM2mObjectInstance) response.getContent()); + } + } + return ObserveResponse.success(new LwM2mObject(getId(), lwM2mObjectInstances)); + } + + // Manage Instance case + final LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); + if (instance == null) + return ObserveResponse.notFound(); + + if (path.getResourceId() == null) { + return instance.observe(server); + } + + // Manage Resource case + if (path.getResourceInstanceId() == null) { + return instance.observe(server, path.getResourceId()); + } + + // Manage Resource Instance case + return instance.observe(server, path.getResourceId(), path.getResourceInstanceId()); + } + + @Override + protected WriteResponse doWrite(LwM2mServer server, WriteRequest request) { + LwM2mPath path = request.getPath(); + + // Manage Instance case + LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); + if (instance == null) + return WriteResponse.notFound(); + + if (path.isObjectInstance()) { + return instance.write(server, request.isReplaceRequest(), (LwM2mObjectInstance) request.getNode()); + } + + // Manage Resource case + if (path.getResourceInstanceId() == null) { + return instance.write(server, request.isReplaceRequest(), path.getResourceId(), + (LwM2mResource) request.getNode()); + } + + // Manage Resource Instance case + return instance.write(server, false, path.getResourceId(), path.getResourceInstanceId(), + ((LwM2mResourceInstance) request.getNode())); + } + + @Override + protected BootstrapWriteResponse doWrite(LwM2mServer server, BootstrapWriteRequest request) { + LwM2mPath path = request.getPath(); + + // Manage Object case + if (path.isObject()) { + for (LwM2mObjectInstance instanceNode : ((LwM2mObject) request.getNode()).getInstances().values()) { + LwM2mInstanceEnabler instanceEnabler = instances.get(instanceNode.getId()); + if (instanceEnabler == null) { + doCreate(server, new CreateRequest(path.getObjectId(), instanceNode)); + } else { + doWrite(server, new WriteRequest(Mode.REPLACE, path.getObjectId(), instanceEnabler.getId(), + instanceNode.getResources().values())); + } + } + return BootstrapWriteResponse.success(); + } + + // Manage Instance case + if (path.isObjectInstance()) { + LwM2mObjectInstance instanceNode = (LwM2mObjectInstance) request.getNode(); + LwM2mInstanceEnabler instanceEnabler = instances.get(path.getObjectInstanceId()); + if (instanceEnabler == null) { + doCreate(server, new CreateRequest(path.getObjectId(), instanceNode)); + } else { + doWrite(server, new WriteRequest(Mode.REPLACE, request.getContentFormat(), path.getObjectId(), + path.getObjectInstanceId(), instanceNode.getResources().values())); + } + return BootstrapWriteResponse.success(); + } + + // Manage resource case + LwM2mResource resource = (LwM2mResource) request.getNode(); + LwM2mInstanceEnabler instanceEnabler = instances.get(path.getObjectInstanceId()); + if (instanceEnabler == null) { + doCreate(server, new CreateRequest(path.getObjectId(), + new LwM2mObjectInstance(path.getObjectInstanceId(), resource))); + } else { + instanceEnabler.write(server, true, path.getResourceId(), resource); + } + return BootstrapWriteResponse.success(); + } + + @Override + protected ExecuteResponse doExecute(LwM2mServer server, ExecuteRequest request) { + LwM2mPath path = request.getPath(); + LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); + if (instance == null) { + return ExecuteResponse.notFound(); + } + return instance.execute(server, path.getResourceId(), request.getArguments()); + } + + @Override + protected DeleteResponse doDelete(LwM2mServer server, DeleteRequest request) { + LwM2mInstanceEnabler deletedInstance = instances.remove(request.getPath().getObjectInstanceId()); + if (deletedInstance != null) { + deletedInstance.onDelete(server); + fireInstancesRemoved(deletedInstance.getId()); + return DeleteResponse.success(); + } + return DeleteResponse.notFound(); + } + + @Override + public BootstrapDeleteResponse doDelete(LwM2mServer server, BootstrapDeleteRequest request) { + if (request.getPath().isRoot() || request.getPath().isObject()) { + if (id == LwM2mId.SECURITY) { + // For security object, we clean everything except bootstrap Server account. + + // Get bootstrap account and store removed instances ids + Entry bootstrapServerAccount = null; + int[] instanceIds = new int[instances.size()]; + int i = 0; + for (Entry instance : instances.entrySet()) { + if (ServersInfoExtractor.isBootstrapServer(instance.getValue())) { + bootstrapServerAccount = instance; + } else { + // Store instance ids + instanceIds[i] = instance.getKey(); + i++; + } + } + // Clear everything + instances.clear(); + + // Put bootstrap account again + if (bootstrapServerAccount != null) { + instances.put(bootstrapServerAccount.getKey(), bootstrapServerAccount.getValue()); + } + + fireInstancesRemoved(instanceIds); + return BootstrapDeleteResponse.success(); + } else if (id == LwM2mId.OSCORE) { + // For OSCORE object, we clean everything except OSCORE object link to bootstrap Server account. + + // Get bootstrap account + LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance( + getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY)); + // Get OSCORE instance ID associated to it + Integer bootstrapOscoreInstanceId = bootstrapInstance != null + ? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance) + : null; + + // if bootstrap server use OSCORE, + // search the OSCORE instance for this ID and store removed instances ids + if (bootstrapOscoreInstanceId != null) { + Entry bootstrapServerOscore = null; + int[] instanceIds = new int[instances.size()]; + int i = 0; + for (Entry instance : instances.entrySet()) { + if (bootstrapOscoreInstanceId.equals(instance.getKey())) { + bootstrapServerOscore = instance; + } else { + // Store instance ids + instanceIds[i] = instance.getKey(); + i++; + } + } + + // Clear everything + instances.clear(); + + // Put bootstrap OSCORE instance again + if (bootstrapServerOscore != null) { + instances.put(bootstrapServerOscore.getKey(), bootstrapServerOscore.getValue()); + } + fireInstancesRemoved(instanceIds); + return BootstrapDeleteResponse.success(); + } + // else delete everything. + } + + // In all other cases, just delete everything + instances.clear(); + // fired instances removed + int[] instanceIds = new int[instances.size()]; + int i = 0; + for (Entry instance : instances.entrySet()) { + instanceIds[i] = instance.getKey(); + i++; + } + fireInstancesRemoved(instanceIds); + + return BootstrapDeleteResponse.success(); + } else if (request.getPath().isObjectInstance()) { + if (id == LwM2mId.SECURITY) { + // For security object, deleting bootstrap Server account is not allowed + LwM2mInstanceEnabler instance = instances.get(request.getPath().getObjectInstanceId()); + if (instance == null) { + return BootstrapDeleteResponse + .badRequest(String.format("Instance %s not found", request.getPath())); + } else if (ServersInfoExtractor.isBootstrapServer(instance)) { + return BootstrapDeleteResponse.badRequest("bootstrap server can not be deleted"); + } + } else if (id == LwM2mId.OSCORE) { + // For OSCORE object, deleting instance linked to Bootstrap account is not allowed + + // Get bootstrap instance + LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance( + getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY)); + // Get OSCORE instance ID associated to it + Integer bootstrapOscoreInstanceId = bootstrapInstance != null + ? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance) + : null; + + if (bootstrapOscoreInstanceId != null + && bootstrapOscoreInstanceId.equals(request.getPath().getObjectInstanceId())) { + return BootstrapDeleteResponse + .badRequest("OSCORE instance linked to bootstrap server can not be deleted"); + } + } + if (null != instances.remove(request.getPath().getObjectInstanceId())) { + fireInstancesRemoved(request.getPath().getObjectInstanceId()); + return BootstrapDeleteResponse.success(); + } else { + return BootstrapDeleteResponse.badRequest(String.format("Instance %s not found", request.getPath())); + } + } + return BootstrapDeleteResponse.badRequest(String.format("unexcepted path %s", request.getPath())); + } + + protected void listenInstance(LwM2mInstanceEnabler instance, final int instanceId) { + instance.addResourceListener(new ResourceListener() { + @Override + public void resourceChanged(LwM2mPath... paths) { + for (LwM2mPath path : paths) { + if (!isValid(instanceId, path)) { + LOG.warn("InstanceEnabler ({}) of object ({}) try to raise a change of {} which seems invalid.", + instanceId, getId(), path); + } + } + fireResourcesChanged(paths); + } + }); + } + + protected boolean isValid(int instanceId, LwM2mPath pathToValidate) { + if (!(pathToValidate.isResource() || pathToValidate.isResourceInstance())) + return false; + + if (pathToValidate.getObjectId() != getId()) { + return false; + } + + if (pathToValidate.getObjectInstanceId() != instanceId) { + return false; + } + + return true; + } + + @Override + public ContentFormat getDefaultEncodingFormat(DownlinkRequest request) { + return defaultContentFormat; + } + + @Override + public void init(LwM2mClient client, LinkFormatHelper linkFormatHelper) { + super.init(client, linkFormatHelper); + this.tbLinkFormatHelper = linkFormatHelper; + for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { + instanceEnabler.setLwM2mClient(client); + } + } + + @Override + public void destroy() { + for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { + if (instanceEnabler instanceof Destroyable) { + ((Destroyable) instanceEnabler).destroy(); + } else if (instanceEnabler instanceof Stoppable) { + ((Stoppable) instanceEnabler).stop(); + } + } + } + + @Override + public void start() { + for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { + if (instanceEnabler instanceof Startable) { + ((Startable) instanceEnabler).start(); + } + } + } + + @Override + public void stop() { + for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { + if (instanceEnabler instanceof Stoppable) { + ((Stoppable) instanceEnabler).stop(); + } + } + } + + @Override + public synchronized WriteAttributesResponse writeAttributes(LwM2mServer server, WriteAttributesRequest request) { + // execute is not supported for bootstrap + if (server.isLwm2mBootstrapServer()) { + return WriteAttributesResponse.methodNotAllowed(); + } + // TODO should be implemented here to be available for all object enabler + // This should be a not implemented error, but this is not defined in the spec. + return WriteAttributesResponse.internalServerError("not implemented"); + } + + + @Override + public synchronized DiscoverResponse discover(LwM2mServer server, DiscoverRequest request) { + + if (server.isLwm2mBootstrapServer()) { + // discover is not supported for bootstrap + return DiscoverResponse.methodNotAllowed(); + } + + if (id == LwM2mId.SECURITY || id == LwM2mId.OSCORE) { + return DiscoverResponse.notFound(); + } + return doDiscover(server, request); + + } + + protected DiscoverResponse doDiscover(LwM2mServer server, DiscoverRequest request) { + + LwM2mPath path = request.getPath(); + if (path.isObject()) { + // Manage discover on object + LwM2mLink[] ObjectLinks = this.tbLinkFormatHelper.getObjectDescription(this, null); + return DiscoverResponse.success(ObjectLinks); + + } else if (path.isObjectInstance()) { + // Manage discover on instance + if (!getAvailableInstanceIds().contains(path.getObjectInstanceId())) + return DiscoverResponse.notFound(); + + LwM2mLink[] instanceLink = this.tbLinkFormatHelper.getInstanceDescription(this, path.getObjectInstanceId(), null); + return DiscoverResponse.success(instanceLink); + + } else if (path.isResource()) { + // Manage discover on resource + if (!getAvailableInstanceIds().contains(path.getObjectInstanceId())) + return DiscoverResponse.notFound(); + + ResourceModel resourceModel = getObjectModel().resources.get(path.getResourceId()); + if (resourceModel == null) + return DiscoverResponse.notFound(); + + if (!getAvailableResourceIds(path.getObjectInstanceId()).contains(path.getResourceId())) + return DiscoverResponse.notFound(); + LwM2mLink resourceLink = this.getResourceAttributes(server, path, resourceModel, this); + if (resourceLink == null) { + resourceLink = this.tbLinkFormatHelper.getResourceDescription(this, path.getObjectInstanceId(), path.getResourceId(), null); + } + return DiscoverResponse.success(new LwM2mLink[] { resourceLink }); + } + return DiscoverResponse.badRequest(null); + } + + protected LwM2mLink getResourceAttributes (LwM2mServer server, LwM2mPath path, ResourceModel resourceModel, LwM2mObjectEnabler objectEnabler) { + LwM2mAttribute attrResource = null; + if (resourceModel.multiple) { + attrResource = getResourceAttributeDim(path, server); + + } + + return attrResource != null ? new LwM2mLink(null, new LwM2mPath(objectEnabler.getId(), path.getObjectInstanceId(), path.getResourceId()), attrResource) : null; + } + + protected LwM2mAttribute getResourceAttributeDim(LwM2mPath path, LwM2mServer server) { + LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); + try { + ReadResponse readResponse = instance.read(server, path.getResourceId()); + if (readResponse.getCode().getCode()==205 && readResponse.getContent() instanceof LwM2mMultipleResource) { + long valueDim = ((LwM2mMultipleResource)readResponse.getContent()).getInstances().size(); + return LwM2mAttributes.create(LwM2mAttributes.DIMENSION, valueDim); + } else { + return null; + } + } catch (Exception e ){ + return null; + } + + } + +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java new file mode 100644 index 0000000000..33cfe5e4ef --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java @@ -0,0 +1,56 @@ +package org.thingsboard.server.transport.lwm2m.client; + +import org.eclipse.leshan.client.resource.BaseInstanceEnablerFactory; +import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.resource.ObjectsInitializer; +import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.model.ObjectModel; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TbObjectsInitializer extends ObjectsInitializer { + + + public TbObjectsInitializer(LwM2mModel model) { + super(model); + } + + public List create(int... objectId) { + List enablers = new ArrayList<>(); + for (int anObjectId : objectId) { + LwM2mObjectEnabler objectEnabler = create(anObjectId); + if (objectEnabler != null) + enablers.add(objectEnabler); + } + return enablers; + } + + public LwM2mObjectEnabler create(int objectId) { + ObjectModel objectModel = model.getObjectModel(objectId); + if (objectModel == null) { + throw new IllegalArgumentException( + "Cannot create object for id " + objectId + " because no model is defined for this id."); + } + return createNodeEnabler(objectModel); + } + + protected LwM2mObjectEnabler createNodeEnabler(ObjectModel objectModel) { + Map instances = new HashMap<>(); + LwM2mInstanceEnabler[] newInstances = createInstances(objectModel); + for (LwM2mInstanceEnabler instance : newInstances) { + // set id if not already set + if (instance.getId() == null) { + int id = BaseInstanceEnablerFactory.generateNewInstanceId(instances.keySet()); + instance.setId(id); + } + instance.setModel(objectModel); + instances.put(instance.getId(), instance); + } + return new TbLwm2mObjectEnabler(objectModel.id, objectModel, instances, getFactoryFor(objectModel), + getContentFormat(objectModel.id)); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java index ea0a1ad4ca..6da48b0a79 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java @@ -99,8 +99,8 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration * {"result":"CONTENT","value":",,,,,,,,,,,,,,,,,,,,,,,"} * If WriteAttributes implemented and WriteAttributes saved - * Discover {"id":"19/0"} - * {"result":"CONTENT","value":"[;dim=2;pmin=10;pmax=60;gt=50;lt=42.2,;pmax=120, , , , , ;lt=45]"} + * Discover {"id":"19"} + * {"result":"CONTENT","value":"[;ver=1.1,;dim=2;pmin=10;pmax=60;gt=50;lt=42.2,;pmax=120, , , , , ;lt=45]"} */ @Test public void testDiscoverInstance_Return_CONTENT_LinksResourcesOnLyExpectedInstance() throws Exception { @@ -117,12 +117,12 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration /** * Discover {"id":"3/0/14"} * If WriteAttributes implemented: - * {"result":"CONTENT","value":";pmax=100, "pmin":10, "ver"=1.0"} + * {"result":"CONTENT","value":";pmax=100, "pmin":10} * If WriteAttributes not implemented: * {"result":"CONTENT","value":""} * Discover {"id":"19_1.1/0/0"} * If WriteAttributes implemented: - * {"result":"CONTENT","value":";pmax=100, "pmin":10, "ver"=1.1"} + * {"result":"CONTENT","value":";pmax=100, "pmin":10} * If WriteAttributes not implemented: * {"result":"CONTENT","value":""} */ diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java index c2d87149de..1e5c9b593a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java @@ -24,7 +24,8 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_15; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6; public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest { @@ -38,7 +39,7 @@ public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MInte */ @Test public void testWriteAttributesResourceWithParametersById_Result_INTERNAL_SERVER_ERROR() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15; sendRPCReadById(expectedPath); String expectedValue = "{\"pmax\":100, \"pmin\":10}"; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); @@ -74,6 +75,46 @@ public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MInte assertTrue(actual.equals(expected)); } + /** + * Class Attributes + * Dimension dim Integer [0:255] + * Number of instances existing for a Multiple-Instance Resource + * 3 + * + * Available Power Sources + * R + * Multiple + * Integer + * 0..7 + * WriteAttributes implemented: Discover {"id":"3/0/6"} -> 'dim' = 3 + * "ver" only for objectId + */ + @Test + public void testDIM_3_0_6_Only_R () throws Exception { + String path = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6; + String actualResult = sendDiscover(path); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String expected = ";dim=3"; + assertTrue(rpcActualResult.get("value").asText().equals(expected)); + + } + /** + * Class Attributes + * Minimum/Maximum Period + * Notes: The Minimum Period Attribute: + * -- indicates the minimum time in seconds the LwM2M Client MUST wait between two notifications. If a notification of an observed Resource is supposed to be generated but it is before pmin expiry, notification MUST be sent as soon as pmin expires. In the absence of this parameter, the Minimum Period is defined by the Default Minimum Period set in the LwM2M Server Account. + * Notes: The Maximum Period Attribute: + * -- indicates the maximum time in seconds the LwM2M Client MAY wait between two notifications. When this "Maximum Period" expires after the last notification, a new notification MUST be sent. In the absence of this parameter, the "Maximum Period" is defined by the Default Maximum Period when set in the LwM2M Server Account or considered as 0 otherwise. The value of 0, means pmax MUST be ignored. The maximum period parameter MUST be greater than the minimum period parameter otherwise pmax will be ignored for the Resource to which such inconsistent timing conditions are applied. + * Object Id = 1 + * Default Minimum Period Id = 2 300 + * Default Maximum Period Id = 3 6000 + */ + @Test + public void testPeriod () { + + } + private String sendRPCExecuteWithValueById(String path, String value) throws Exception { String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}"; return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); @@ -84,4 +125,9 @@ public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MInte return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); } + private String sendDiscover(String path) throws Exception { + String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; + return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); + } + } diff --git a/application/src/test/resources/lwm2m/3.xml b/application/src/test/resources/lwm2m/3.xml index 724fc4cb33..e71c2045c2 100644 --- a/application/src/test/resources/lwm2m/3.xml +++ b/application/src/test/resources/lwm2m/3.xml @@ -64,7 +64,7 @@ LEGAL DISCLAIMER 3 urn:oma:lwm2m:oma:3:1.0 1.1 - 1.0 + 1.2 Single Mandatory From 6bcb00b6b90f392270a4709f93d1846233131385 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 4 Jan 2024 12:16:06 +0200 Subject: [PATCH 06/23] lwm2m: add test, Write Attributes implemented and read Attributes (Discover) --- .../lwm2m/client/SimpleLwM2MDevice.java | 2 +- .../lwm2m/client/TbLwm2mObjectEnabler.java | 126 ++++++++-- .../lwm2m/client/TbObjectsInitializer.java | 15 ++ .../rpc/AbstractRpcLwM2MIntegrationTest.java | 4 + ...ntegrationDiscoverWriteAttributesTest.java | 226 ++++++++++++++++++ .../sql/RpcLwm2mIntegrationObserveTest.java | 9 +- ...pcLwm2mIntegrationWriteAttributesTest.java | 133 ----------- 7 files changed, 359 insertions(+), 156 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java delete mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java index e7e2320b38..89833a43a6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java @@ -47,7 +47,7 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl private static final int min = 5; private static final int max = 50; private static final PrimitiveIterator.OfInt randomIterator = new Random().ints(min,max + 1).iterator(); - private static final List supportedResources = Arrays.asList(0, 1, 2, 3, 6, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21); + private static final List supportedResources = Arrays.asList(0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21); public SimpleLwM2MDevice() { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java index a0c1333069..5fa55f24a3 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java @@ -1,3 +1,18 @@ +/** + * 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.lwm2m.client; import org.eclipse.leshan.client.LwM2mClient; @@ -5,7 +20,6 @@ import org.eclipse.leshan.client.resource.BaseObjectEnabler; import org.eclipse.leshan.client.resource.DummyInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mInstanceEnablerFactory; -import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; import org.eclipse.leshan.client.resource.listener.ResourceListener; import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.client.servers.ServersInfoExtractor; @@ -16,6 +30,7 @@ import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Stoppable; import org.eclipse.leshan.core.link.lwm2m.LwM2mLink; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; @@ -54,6 +69,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -66,21 +82,24 @@ public class TbLwm2mObjectEnabler extends BaseObjectEnabler implements Destroyab private static Logger LOG = LoggerFactory.getLogger(DummyInstanceEnabler.class); protected Map instances; + protected LwM2mInstanceEnablerFactory instanceFactory; protected ContentFormat defaultContentFormat; private LinkFormatHelper tbLinkFormatHelper; - + protected Map lwM2mAttributes; public TbLwm2mObjectEnabler(int id, ObjectModel objectModel, Map instances, LwM2mInstanceEnablerFactory instanceFactory, ContentFormat defaultContentFormat) { super(id, objectModel); this.instances = new HashMap<>(instances); + ; this.instanceFactory = instanceFactory; this.defaultContentFormat = defaultContentFormat; for (Entry entry : this.instances.entrySet()) { instances.put(entry.getKey(), entry.getValue()); listenInstance(entry.getValue(), entry.getKey()); } + this.lwM2mAttributes = new HashMap<>(); } public TbLwm2mObjectEnabler(int id, ObjectModel objectModel) { @@ -555,11 +574,55 @@ public class TbLwm2mObjectEnabler extends BaseObjectEnabler implements Destroyab if (server.isLwm2mBootstrapServer()) { return WriteAttributesResponse.methodNotAllowed(); } - // TODO should be implemented here to be available for all object enabler - // This should be a not implemented error, but this is not defined in the spec. - return WriteAttributesResponse.internalServerError("not implemented"); +// return WriteAttributesResponse.internalServerError("not implemented"); + return doWriteAttributes(server, request); } + /** + * Class Attributes + * - pmin (def = 0(sec)) Integer Resource/Object Instance/Object Readable Resource + * - pmax (def = -- ) Integer Resource/Object Instance/Object Readable Resource + * - Greater Than gt (def = -- ) Float Resource Numerical&Readable Resource + * - Less Than lt (def = -- ) Float Resource Numerical&Readable Resource + * - Step st (def = -- ) Float Resource Numerical&Readable Resource + */ + public WriteAttributesResponse doWriteAttributes(LwM2mServer server, WriteAttributesRequest request) { + LwM2mPath lwM2mPath = request.getPath(); + LwM2mAttributeSet attributeSet = lwM2mAttributes.get(lwM2mPath); + Map > attributes = new HashMap<>(); + + for (LwM2mAttribute attr : request.getAttributes().getLwM2mAttributes()) { + if (attr.getName().equals("pmax") || attr.getName().equals("pmin")) { + if (lwM2mPath.isObject() || lwM2mPath.isObjectInstance() || lwM2mPath.isResource()) { + attributes.put(attr.getName(), attr); + } else { + return WriteAttributesResponse.badRequest("Attribute " + attr.getName() + " can be used for only Resource/Object Instance/Object."); + } + } else if (attr.getName().equals("gt") || attr.getName().equals("lt") || attr.getName().equals("st")) { + if (lwM2mPath.isResource()) { + attributes.put(attr.getName(), attr); + } else { + return WriteAttributesResponse.badRequest("Attribute " + attr.getName() + " can be used for only Resource."); + } + } + } + if (attributes.size()>0){ + if (attributeSet == null) { + attributeSet = new LwM2mAttributeSet(attributes.values()); + } else { + Iterable> lwM2mAttributeIterable = attributeSet.getLwM2mAttributes(); + Map > attributesOld = new HashMap<>(); + for (LwM2mAttribute attr : lwM2mAttributeIterable) { + attributesOld.put(attr.getName(), attr); + } + attributesOld.putAll(attributes); + attributeSet = new LwM2mAttributeSet(attributesOld.values()); + } + lwM2mAttributes.put(lwM2mPath, attributeSet); + return WriteAttributesResponse.success(); + } + return WriteAttributesResponse.internalServerError("not implemented"); + } @Override public synchronized DiscoverResponse discover(LwM2mServer server, DiscoverRequest request) { @@ -580,8 +643,7 @@ public class TbLwm2mObjectEnabler extends BaseObjectEnabler implements Destroyab LwM2mPath path = request.getPath(); if (path.isObject()) { - // Manage discover on object - LwM2mLink[] ObjectLinks = this.tbLinkFormatHelper.getObjectDescription(this, null); + LwM2mLink[] ObjectLinks = linkUpdateAttributes(this.tbLinkFormatHelper.getObjectDescription(this, null), server); return DiscoverResponse.success(ObjectLinks); } else if (path.isObjectInstance()) { @@ -589,7 +651,7 @@ public class TbLwm2mObjectEnabler extends BaseObjectEnabler implements Destroyab if (!getAvailableInstanceIds().contains(path.getObjectInstanceId())) return DiscoverResponse.notFound(); - LwM2mLink[] instanceLink = this.tbLinkFormatHelper.getInstanceDescription(this, path.getObjectInstanceId(), null); + LwM2mLink[] instanceLink = linkUpdateAttributes(this.tbLinkFormatHelper.getInstanceDescription(this, path.getObjectInstanceId(), null), server); return DiscoverResponse.success(instanceLink); } else if (path.isResource()) { @@ -603,23 +665,52 @@ public class TbLwm2mObjectEnabler extends BaseObjectEnabler implements Destroyab if (!getAvailableResourceIds(path.getObjectInstanceId()).contains(path.getResourceId())) return DiscoverResponse.notFound(); - LwM2mLink resourceLink = this.getResourceAttributes(server, path, resourceModel, this); - if (resourceLink == null) { - resourceLink = this.tbLinkFormatHelper.getResourceDescription(this, path.getObjectInstanceId(), path.getResourceId(), null); - } + + LwM2mLink resourceLink = linkAddAttribute( + this.tbLinkFormatHelper.getResourceDescription(this, path.getObjectInstanceId(), path.getResourceId(), null), + server); return DiscoverResponse.success(new LwM2mLink[] { resourceLink }); } return DiscoverResponse.badRequest(null); } - protected LwM2mLink getResourceAttributes (LwM2mServer server, LwM2mPath path, ResourceModel resourceModel, LwM2mObjectEnabler objectEnabler) { - LwM2mAttribute attrResource = null; - if (resourceModel.multiple) { - attrResource = getResourceAttributeDim(path, server); + private LwM2mLink[] linkUpdateAttributes(LwM2mLink[] links, LwM2mServer server) { + return Arrays.stream(links) + .map(link -> linkAddAttribute(link, server)) + .toArray(LwM2mLink[]::new); + } + + private LwM2mLink linkAddAttribute(LwM2mLink link, LwM2mServer server) { + + LwM2mAttributeSet lwM2mAttributeSetDop = null; + if (this.lwM2mAttributes.get(link.getPath())!= null){ + lwM2mAttributeSetDop = this.lwM2mAttributes.get(link.getPath()); + } + LwM2mAttribute resourceAttributeDim = getResourceAttributes (server, link.getPath()); + Map > attributes = new HashMap<>(); + if (link.getAttributes() != null) { + for (LwM2mAttribute attr : link.getAttributes().getLwM2mAttributes()) { + attributes.put(attr.getName(), attr); + } + } + if (lwM2mAttributeSetDop != null) { + for (LwM2mAttribute attr : lwM2mAttributeSetDop.getLwM2mAttributes()) { + attributes.put(attr.getName(), attr); + } + } + if (resourceAttributeDim != null) { + attributes.put(resourceAttributeDim.getName(), resourceAttributeDim); } + return new LwM2mLink(link.getRootPath(), link.getPath(), attributes.values()); + } - return attrResource != null ? new LwM2mLink(null, new LwM2mPath(objectEnabler.getId(), path.getObjectInstanceId(), path.getResourceId()), attrResource) : null; + protected LwM2mAttribute getResourceAttributes (LwM2mServer server, LwM2mPath path) { + ResourceModel resourceModel = getObjectModel().resources.get(path.getResourceId()); + if (path.isResource() && resourceModel.multiple) { + return getResourceAttributeDim(path, server); + } + return null; } protected LwM2mAttribute getResourceAttributeDim(LwM2mPath path, LwM2mServer server) { @@ -635,7 +726,6 @@ public class TbLwm2mObjectEnabler extends BaseObjectEnabler implements Destroyab } catch (Exception e ){ return null; } - } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java index 33cfe5e4ef..78322bcd7c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java @@ -1,3 +1,18 @@ +/** + * 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.lwm2m.client; import org.eclipse.leshan.client.resource.BaseInstanceEnablerFactory; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index ddd1329254..e3c0c680cf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -37,6 +37,7 @@ import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; @@ -62,6 +63,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected String objectInstanceIdVer_1; protected String objectIdVer_0; + protected String objectIdVer_1; protected String objectIdVer_2; private static final Predicate PREDICATE_3 = path -> (!((String) path).startsWith("/" + TEMPERATURE_SENSOR) && ((String) path).startsWith("/" + DEVICE)); protected String objectIdVer_3; @@ -106,7 +108,9 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg } }); String ver_Id_0 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_0).version; + String ver_Id_1 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_1).version; objectIdVer_0 = "/" + OBJECT_ID_0 + "_" + ver_Id_0; + objectIdVer_1 = "/" + OBJECT_ID_1 + "_" + ver_Id_1; objectIdVer_2 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + ACCESS_CONTROL)).findFirst().get(); objectIdVer_3 = (String) expectedObjectIdVers.stream().filter(PREDICATE_3).findFirst().get(); objectIdVer_19 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + BINARY_APP_DATA_CONTAINER)).findFirst().get(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java new file mode 100644 index 0000000000..536770cfd3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java @@ -0,0 +1,226 @@ +/** + * 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.lwm2m.rpc.sql; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.eclipse.leshan.core.ResponseCode; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; + + +public class RpcLwm2mIntegrationDiscoverWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest { + + /** + * WriteAttributes {"id":"/3_1.2/0/6","attributes":{"pmax":100, "pmin":10}} + * if not implemented: + * {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"} + * if implemented: + * {"result":"BAD_REQUEST","error":"Attribute pmax can be used for only Resource/Object Instance/Object."} + */ + @Test + public void testWriteAttributesResourceWithParametersByResourceInstanceId_Result_BAD_REQUEST() throws Exception { + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6 + "/1"; + String expectedValue = "{\"pmax\":100, \"pmin\":10}"; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); + String expected = "Attribute pmax can be used for only Resource/Object Instance/Object."; + String actual = rpcActualResult.get("error").asText(); + assertTrue(actual.equals(expected)); + } + /** + * WriteAttributes {"id":"/3_1.2/0/6","attributes":{"pmax":100, "pmin":10}} + * if not implemented: + * {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"} + * if implemented: + * {"result":"BAD_REQUEST","error":"Attribute pmax can be used for only Resource/Object Instance/Object."} + */ + @Test + public void testWriteAttributeResourceDimWithParametersByResourceId_Result_BAD_REQUEST() throws Exception { + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6; + String expectedValue = "{\"dim\":3}"; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); + String expected = "Attribute dim is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request."; + String actual = rpcActualResult.get("error").asText(); + assertTrue(actual.equals(expected)); + } + + @Test + public void testWriteAttributesResourceVerWithParametersById_Result_BAD_REQUEST() throws Exception { + String expectedPath = objectIdVer_3; + String expectedValue = "{\"ver\":1.3}"; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); + String expected = "Attribute ver is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request."; + String actual = rpcActualResult.get("error").asText(); + assertTrue(actual.equals(expected)); + } + + @Test + public void testWriteAttributesResourceServerUriWithParametersById_Result_BAD_REQUEST() throws Exception { + String expectedPath = objectInstanceIdVer_1; + String actualResult = sendRPCReadById(expectedPath); + String expectedValue = "{\"uri\":\"coaps://localhost:5690\"}"; + actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); + String expected = "Attribute uri is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request."; + String actual = rpcActualResult.get("error").asText(); + assertTrue(actual.equals(expected)); + } + + /** + * Class Attributes + * Dimension dim Integer [0:255] + * Number of instances existing for a Multiple-Instance Resource + * 3 + * + * Available Power Sources + * R + * Multiple + * Integer + * 0..7 + * WriteAttributes implemented: Discover {"id":"3/0/6"} -> 'dim' = 3 + * "ver" only for objectId + */ + @Test + public void testReadDIM_3_0_6_Only_R () throws Exception { + String path = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6; + String actualResult = sendDiscover(path); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String expected = ";dim=3"; + assertTrue(rpcActualResult.get("value").asText().equals(expected)); + + } + + + /** + * Class Attributes + * Object Version ver Object + * Provide the version of the associated Object. + * "ver" only for objectId + */ + @Test + public void testReadVer () throws Exception { + String path = objectIdVer_3; + String actualResult = sendDiscover(path); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String expected = ";ver=1.2"; + assertTrue(rpcActualResult.get("value").asText().contains(expected)); + } + + /** + * Class Attributes + * Minimum/Maximum Period pmin/pmax + * Notes: The Minimum Period Attribute: + * -- indicates the minimum time in seconds the LwM2M Client MUST wait between two notifications. If a notification of an observed Resource is supposed to be generated but it is before pmin expiry, notification MUST be sent as soon as pmin expires. In the absence of this parameter, the Minimum Period is defined by the Default Minimum Period set in the LwM2M Server Account. + * Notes: The Maximum Period Attribute: + * -- indicates the maximum time in seconds the LwM2M Client MAY wait between two notifications. When this "Maximum Period" expires after the last notification, a new notification MUST be sent. In the absence of this parameter, the "Maximum Period" is defined by the Default Maximum Period when set in the LwM2M Server Account or considered as 0 otherwise. The value of 0, means pmax MUST be ignored. The maximum period parameter MUST be greater than the minimum period parameter otherwise pmax will be ignored for the Resource to which such inconsistent timing conditions are applied. + * Greater Than gt Resource + * Less Than lt Resource + * Step st Resource + * + * Object Id = 1 + * Default Minimum Period Id = 2 300 or 0 + * Default Maximum Period Id = 3 6000 or "-" + * ;pmax=65, , <3/0/2>, , , + * <3/0/6>;dim=8,<3/0/7>;gt=50;lt=42.2;st=0.5,<3/0/8>;... + */ + @Test + public void testWriteAttributesPeriodLtGt () throws Exception { + String expectedPath = objectInstanceIdVer_3; + String expectedValue = "{\"pmax\":60}"; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + expectedPath = objectInstanceIdVer_3; + expectedValue = "{\"pmax\":65}"; + actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_7; + expectedValue ="{\"gt\":50, \"lt\":42.2, \"st\":0.5}"; + actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + // ObjectId + expectedPath = objectIdVer_3; + actualResult = sendDiscover(expectedPath); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + // String expected = ";ver=1.2,;pmax=60,,,,,;dim=3,;st=0.5;lt=42.2;gt=50.0,,,,;dim=1,,,,,,,,,"; + String expected = ";ver=1.2,;pmax=65"; + assertTrue(rpcActualResult.get("value").asText().contains(expected)); + expected = ";dim=3,;st=0.5;lt=42.2;gt=50.0"; + assertTrue(rpcActualResult.get("value").asText().contains(expected)); + // ObjectInstanceId + expectedPath = objectInstanceIdVer_3; + actualResult = sendDiscover(expectedPath); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + expected = ";pmax=65"; + assertTrue(rpcActualResult.get("value").asText().contains(expected)); + expected = ";dim=3,;st=0.5;lt=42.2;gt=50.0"; + assertTrue(rpcActualResult.get("value").asText().contains(expected)); + // ResourceId + expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6; + actualResult = sendDiscover(expectedPath); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + expected = ";dim=3"; + assertTrue(rpcActualResult.get("value").asText().contains(expected)); + expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_7; + actualResult = sendDiscover(expectedPath); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + expected = ";st=0.5;lt=42.2;gt=50.0"; + assertTrue(rpcActualResult.get("value").asText().contains(expected)); + // ResourceInstanceId + expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6+ "/1"; + actualResult = sendDiscover(expectedPath); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText()); + expected = "InvalidRequestException: Discover request cannot target resource instance path: /3/0/6/1"; + assertTrue(rpcActualResult.get("error").asText().contains(expected)); + } + + private String sendRPCExecuteWithValueById(String path, String value) throws Exception { + String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}"; + return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); + } + + private String sendRPCReadById(String path) throws Exception { + String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; + return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); + } + + private String sendDiscover(String path) throws Exception { + String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; + return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index 9a17978e83..4d8f58f6f0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -66,7 +66,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testObserveSingleResourceWithout_IdVer_1_0_Result_CONTENT_Value_SingleResource() throws Exception { - String expectedId = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; + String expectedId = objectInstanceIdVer_9 + "/" + RESOURCE_ID_0; String actualResult = sendRpcObserve("Observe", fromVersionedIdToObjectId(expectedId)); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); @@ -147,16 +147,17 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT /** * Repeated request on Observe - * Observe {"id":"/3/0/9"} + * Observe {"id":"/3_1.2/0/9"} * @throws Exception */ @Test public void testObserveRepeatedRequestObserveOnDevice_Result_BAD_REQUEST_ErrorMsg_AlreadyRegistered() throws Exception { String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; - sendRpcObserve("Observe", fromVersionedIdToObjectId(idVer_3_0_0)); - sendRpcObserve("ObserveReadAll", null); String actualResult = sendRpcObserve("Observe", idVer_3_0_0); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + actualResult = sendRpcObserve("Observe", idVer_3_0_0); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); String expected = "Observation is already registered!"; assertEquals(expected, rpcActualResult.get("error").asText()); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java deleted file mode 100644 index 1e5c9b593a..0000000000 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java +++ /dev/null @@ -1,133 +0,0 @@ -/** - * 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.lwm2m.rpc.sql; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.eclipse.leshan.core.ResponseCode; -import org.junit.Test; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_15; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6; - - -public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest { - - /** - * WriteAttributes {"id":"/3/0/14","attributes":{"pmax":100, "pmin":10}} - * if not implemented: - * {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"} - * if implemented: - * {"result":"CHANGED"} - */ - @Test - public void testWriteAttributesResourceWithParametersById_Result_INTERNAL_SERVER_ERROR() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15; - sendRPCReadById(expectedPath); - String expectedValue = "{\"pmax\":100, \"pmin\":10}"; - String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText()); - String expected = "not implemented"; - String actual = rpcActualResult.get("error").asText(); - assertTrue(actual.equals(expected)); - } - @Test - public void testWriteAttributesResourceVerWithParametersById_Result_BAD_REQUEST() throws Exception { - String expectedPath = objectIdVer_3; - String actualResult = sendRPCReadById(expectedPath); - String expectedValue = "{\"ver\":1.3}"; - actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); - String expected = "Attribute ver is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request."; - String actual = rpcActualResult.get("error").asText(); - assertTrue(actual.equals(expected)); - } - - @Test - public void testWriteAttributesResourceServerUriWithParametersById_Result_BAD_REQUEST() throws Exception { - String expectedPath = objectInstanceIdVer_1; - String actualResult = sendRPCReadById(expectedPath); - String expectedValue = "{\"uri\":\"coaps://localhost:5690\"}"; - actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); - String expected = "Attribute uri is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request."; - String actual = rpcActualResult.get("error").asText(); - assertTrue(actual.equals(expected)); - } - - /** - * Class Attributes - * Dimension dim Integer [0:255] - * Number of instances existing for a Multiple-Instance Resource - * 3 - * - * Available Power Sources - * R - * Multiple - * Integer - * 0..7 - * WriteAttributes implemented: Discover {"id":"3/0/6"} -> 'dim' = 3 - * "ver" only for objectId - */ - @Test - public void testDIM_3_0_6_Only_R () throws Exception { - String path = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6; - String actualResult = sendDiscover(path); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String expected = ";dim=3"; - assertTrue(rpcActualResult.get("value").asText().equals(expected)); - - } - /** - * Class Attributes - * Minimum/Maximum Period - * Notes: The Minimum Period Attribute: - * -- indicates the minimum time in seconds the LwM2M Client MUST wait between two notifications. If a notification of an observed Resource is supposed to be generated but it is before pmin expiry, notification MUST be sent as soon as pmin expires. In the absence of this parameter, the Minimum Period is defined by the Default Minimum Period set in the LwM2M Server Account. - * Notes: The Maximum Period Attribute: - * -- indicates the maximum time in seconds the LwM2M Client MAY wait between two notifications. When this "Maximum Period" expires after the last notification, a new notification MUST be sent. In the absence of this parameter, the "Maximum Period" is defined by the Default Maximum Period when set in the LwM2M Server Account or considered as 0 otherwise. The value of 0, means pmax MUST be ignored. The maximum period parameter MUST be greater than the minimum period parameter otherwise pmax will be ignored for the Resource to which such inconsistent timing conditions are applied. - * Object Id = 1 - * Default Minimum Period Id = 2 300 - * Default Maximum Period Id = 3 6000 - */ - @Test - public void testPeriod () { - - } - - private String sendRPCExecuteWithValueById(String path, String value) throws Exception { - String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}"; - return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); - } - - private String sendRPCReadById(String path) throws Exception { - String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; - return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); - } - - private String sendDiscover(String path) throws Exception { - String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; - return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); - } - -} From d59bab4f0f93fdfab24140b94965eba06c7fee5a Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 5 Jan 2024 12:34:55 +0200 Subject: [PATCH 07/23] lwm2m: add to Client Write Attributes implemented and read Attributes (Discover) only for WriteAttributes test --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 29 ++++++++++++------- .../lwm2m/client/LwM2MTestClient.java | 6 ++-- .../rpc/AbstractRpcLwM2MIntegrationTest.java | 5 ++-- .../sql/RpcLwm2mIntegrationObserveTest.java | 22 ++++++-------- .../AbstractSecurityLwM2MIntegrationTest.java | 1 - 5 files changed, 33 insertions(+), 30 deletions(-) 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 4378c3758e..9cd6a66cb5 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 @@ -17,6 +17,8 @@ package org.thingsboard.server.transport.lwm2m; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.eclipse.californium.elements.config.Configuration; @@ -61,6 +63,7 @@ import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; @@ -87,6 +90,7 @@ import static org.eclipse.californium.core.config.CoapConfig.COAP_PORT; import static org.eclipse.californium.core.config.CoapConfig.COAP_SECURE_PORT; import static org.eclipse.leshan.client.object.Security.noSec; import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertEquals; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_STARTED; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_SUCCESS; @@ -177,6 +181,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte protected ScheduledExecutorService executor; protected LwM2MTestClient lwM2MTestClient; private String[] resources; + protected String deviceId; + protected boolean isWriteAttribute = false; @Before public void startInit() throws Exception { @@ -232,7 +238,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte getWsClient().registerWaitForUpdate(); createNewClient(security, null, coapConfig, false, endpoint); - awaitObserveReadAll(0, device.getId().getId().toString()); + deviceId = device.getId().getId().toString(); + awaitObserveReadAll(0, deviceId); String msg = getWsClient().waitForUpdate(); EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class); @@ -305,7 +312,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte try (ServerSocket socket = new ServerSocket(0)) { int clientPort = socket.getLocalPort(); lwM2MTestClient.init(security, securityBs, coapConfig, clientPort, isRpc, - this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest); + this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest, isWriteAttribute); } } @@ -398,15 +405,15 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception { await("ObserveReadAll after start client: countObserve " + cntObserve) .atMost(40, TimeUnit.SECONDS) - .until(() -> { - String actualResultReadAll = sendObserve("ObserveReadAll", null, deviceIdStr); - ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); - Assert.assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); - String actualValuesReadAll = rpcActualResultReadAll.get("value").asText(); - log.warn("ObserveReadAll: [{}]", actualValuesReadAll); - int actualCntObserve = "[]".equals(actualValuesReadAll) ? 0 : actualValuesReadAll.split(",").length; - return cntObserve == actualCntObserve; - }); + .until(() -> cntObserve == getCntObserveAll(deviceIdStr)); + } + + protected int getCntObserveAll(String deviceIdStr) throws Exception { + String actualResultBefore = sendObserve("ObserveReadAll", null, deviceIdStr); + ObjectNode rpcActualResultBefore = JacksonUtil.fromString(actualResultBefore, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultBefore.get("result").asText()); + JsonElement element = JsonUtils.parse(rpcActualResultBefore.get("value").asText()); + return element.isJsonArray() ? ((JsonArray)element).size() : 0; } protected String sendObserve(String method, String params, String deviceIdStr) throws Exception { 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 19f32d175f..59631945f2 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 @@ -36,6 +36,7 @@ import org.eclipse.leshan.client.observer.LwM2mClientObserver; import org.eclipse.leshan.client.resource.DummyInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.resource.ObjectsInitializer; import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter; import org.eclipse.leshan.client.send.ManualDataSender; import org.eclipse.leshan.client.servers.LwM2mServer; @@ -123,7 +124,7 @@ public class LwM2MTestClient { public void init(Security security, Security securityBs,Configuration coapConfig, int port, boolean isRpc, LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, - LwM2mClientContext clientContext) throws InvalidDDFFileException, IOException { + LwM2mClientContext clientContext, boolean isWriteAttribute) throws InvalidDDFFileException, IOException { Assert.assertNull("client already initialized", leshanClient); this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; this.clientContext = clientContext; @@ -132,8 +133,7 @@ public class LwM2MTestClient { models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); } LwM2mModel model = new StaticModel(models); - TbObjectsInitializer initializer = new TbObjectsInitializer(model); - + ObjectsInitializer initializer = isWriteAttribute ? new TbObjectsInitializer(model) : new ObjectsInitializer(model); if (securityBs != null && security != null) { // SECURITY security.setId(serverId); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index e3c0c680cf..9d6a9d7a1d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -55,7 +55,6 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected final LinkParser linkParser = new DefaultLwM2mLinkParser(); protected String OBSERVE_ATTRIBUTES_WITH_PARAMS_RPC; - protected String deviceId; public Set expectedObjects; public Set expectedObjectIdVers; public Set expectedInstances; @@ -84,6 +83,9 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg @Before public void startInitRPC() throws Exception { + if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationDiscoverWriteAttributesTest")){ + isWriteAttribute = true; + } initRpc(); } @@ -154,7 +156,6 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg deviceId = device.getId().getId().toString(); lwM2MTestClient.start(true); -// awaitObserveReadAll(2, true, device.getId().getId().toString()); } protected String pathIdVerToObjectId(String pathIdVer) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index 4d8f58f6f0..d9679c92c4 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -36,28 +36,24 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fr public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest { /** - * ObserveReadAll&ObserveReadAll + * ObserveReadAll&ObserveCancelAll * @throws Exception */ @Test public void testObserveReadAllNothingObservation_Result_CONTENT_Value_Count_0() throws Exception { + awaitObserveReadAll(2, deviceId); String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; - sendRpcObserve("Observe", fromVersionedIdToObjectId(idVer_3_0_0)); - String actualResultBefore = sendRpcObserve("ObserveReadAll", null); - ObjectNode rpcActualResultBefore = JacksonUtil.fromString(actualResultBefore, ObjectNode.class); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultBefore.get("result").asText()); - int cntObserveBefore = rpcActualResultBefore.get("value").asText().split(",").length; - assertTrue(cntObserveBefore > 0); - String actualResult = sendRpcObserve("ObserveCancelAll", null); + String actualResult = sendRpcObserve("Observe", idVer_3_0_0); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + assertTrue(rpcActualResult.get("value").asText().contains("LwM2mSingleResource")); + assertEquals(3, getCntObserveAll(deviceId)); + actualResult = sendRpcObserve("ObserveCancelAll", null); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); int cntObserveCancelAll = Integer.parseInt(rpcActualResult.get("value").asText()); assertTrue(cntObserveCancelAll > 0); - String actualResultAfter = sendRpcObserve("ObserveReadAll", null); - ObjectNode rpcActualResultAfter = JacksonUtil.fromString(actualResultAfter, ObjectNode.class); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultAfter.get("result").asText()); - String expectResultAfter = "[]"; - assertEquals( expectResultAfter, rpcActualResultAfter.get("value").asText()); + assertEquals(0, getCntObserveAll(deviceId)); } /** diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index b97adf035c..7b62023c63 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -196,7 +196,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M boolean isStartLw) throws Exception { createDeviceProfile(transportConfiguration); final Device device = createDevice(deviceCredentials, endpoint); - device.getId().getId().toString(); createNewClient(security, securityBs, coapConfig, true, endpoint); lwM2MTestClient.start(isStartLw); if (isAwaitObserveReadAll) { From 2b82768225415545e20e0db15af0af114b4ae7b9 Mon Sep 17 00:00:00 2001 From: nick Date: Mon, 8 Jan 2024 16:00:05 +0200 Subject: [PATCH 08/23] lwm2m: add Redis --- .../lwm2m/client/LwM2MTestClient.java | 1 - .../LwM2MTransportBootstrapService.java | 60 ----- .../server/DefaultLwM2mTransportService.java | 30 --- .../store/TbLwM2mRedisRegistrationStore.java | 224 ++++++++++-------- 4 files changed, 124 insertions(+), 191 deletions(-) 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 59631945f2..911a47d7b4 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 @@ -255,7 +255,6 @@ public class LwM2MTestClient { */ boolean reconnectOnUpdate = false; engineFactory.setReconnectOnUpdate(reconnectOnUpdate); -// engineFactory.setReconnectOnUpdate(false); // old engineFactory.setResumeOnConnect(true); // new /** diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index 96a02bdf97..1f1382ecf5 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -128,26 +128,6 @@ public class LwM2MTransportBootstrapService { /* Create DTLS Config */ this.setServerWithCredentials(builder); -// DtlsConnectorConfig dtlsConfig; -// try { -// dtlsConfig = dtlsConfigBuilder.build(); -// } catch (IllegalStateException e) { -// log.warn("Unable to create DTLS config for endpont {}.", endpointUri.toString(), e); -// return null; -// } -// -// Connector dTLSConnector = new DTLSConnector(dtlsConfig); - -// endpointsBuilder.setConnector(dTLSConnector); -// endpointsBuilder.setConfiguration(serverCoapConfig); - -// endpointsBuilder.setLoggingTag(String.format("[%s]", "/" + "options.getUriPathString()")); -// endpointsBuilder.setEndpointContextMatcher(new Lwm2mEndpointContextMatcher()); - - /* Create credentials */ - - - // Set Californium Configuration endpointsBuilder.setConfiguration(serverCoapConfig); @@ -161,46 +141,6 @@ public class LwM2MTransportBootstrapService { InetSocketAddress coapsAddr = new InetSocketAddress(bootstrapConfig.getSecureHost(), bootstrapConfig.getSecurePort()); endpointsBuilder.addEndpoint(coapsAddr, Protocol.COAPS); - -// builder.setLocalAddress(config.getHost(), config.getPort()); -// builder.setLocalSecureAddress(config.getSecureHost(), config.getSecurePort()); -// builder.setDecoder(new DefaultLwM2mDecoder()); - /* Use a magic converter to support bad type send by the UI. */ -// builder.setEncoder(new DefaultLwM2mEncoder(LwM2mValueConverterImpl.getInstance())); - - /* Create CoAP Config */ -// builder.setCoapConfig(getCoapConfig(config.getPort(), config.getSecurePort(), config)); - -// -// /* Set securityStore with new registrationStore */ -// builder.setSecurityStore(securityStore); -// builder.setRegistrationStore(registrationStore); -// -// -// // Create LWM2M server -// builder.setEndpointsProviders(endpointsBuilder.build()); -// -// -// -// builder.setLocalAddress(bootstrapConfig.getHost(), bootstrapConfig.getPort()); -// builder.setLocalSecureAddress(bootstrapConfig.getSecureHost(), bootstrapConfig.getSecurePort()); -// -// /* Create CoAP Config */ -// builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig)); -// -// -// /* Create and Set DTLS Config */ -// DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig)); -// -// dtlsConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, serverConfig.isRecommendedSupportedGroups()); -// dtlsConfig.set(DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, serverConfig.isRecommendedCiphers()); -// dtlsConfig.set(DTLS_RETRANSMISSION_TIMEOUT, serverConfig.getDtlsRetransmissionTimeout(), MILLISECONDS); -// dtlsConfig.set(DTLS_ROLE, SERVER_ONLY); -// setServerWithCredentials(builder, dtlsConfig); -// -// /* Set DTLS Config */ -// builder.setDtlsConfig(dtlsConfig); - /* Set securityStore with new ConfigStore */ builder.setConfigStore(lwM2MInMemoryBootstrapConfigStore); 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 5c2045f61a..ab94157ec1 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 @@ -182,29 +182,6 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { /* Create DTLS Config */ this.setServerWithCredentials(builder); - - -// DtlsConnectorConfig dtlsConfig; -// try { -// dtlsConfig = dtlsConfigBuilder.build(); -// } catch (IllegalStateException e) { -// log.warn("Unable to create DTLS config for endpont {}.", endpointUri.toString(), e); -// return null; -// } -// -// Connector dTLSConnector = new DTLSConnector(dtlsConfig); - -// endpointsBuilder.setConnector(dTLSConnector); -// endpointsBuilder.setConfiguration(serverCoapConfig); - -// endpointsBuilder.setLoggingTag(String.format("[%s]", "/" + "options.getUriPathString()")); -// endpointsBuilder.setEndpointContextMatcher(new Lwm2mEndpointContextMatcher()); - - /* Create credentials */ - - - - // Set Californium Configuration endpointsBuilder.setConfiguration(serverCoapConfig); @@ -220,13 +197,6 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { builder.setDecoder(new DefaultLwM2mDecoder(true)); builder.setEncoder(new DefaultLwM2mEncoder(true)); -// builder.setDecoder(new DefaultLwM2mDecoder()); -// /* Use a magic converter to support bad type send by the UI. */ -// builder.setEncoder(new DefaultLwM2mEncoder(LwM2mValueConverterImpl.getInstance())); - - /* Create CoAP Config */ -// builder.setCoapConfig(getCoapConfig(config.getPort(), config.getSecurePort(), config)); - // Create LWM2M server builder.setEndpointsProviders(endpointsBuilder.build()); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index fa44c2a93b..d8be30f86d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -15,11 +15,11 @@ */ package org.thingsboard.server.transport.lwm2m.server.store; +import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.Token; import org.eclipse.californium.core.network.serialization.UdpDataParser; import org.eclipse.californium.core.network.serialization.UdpDataSerializer; import org.eclipse.californium.core.observe.ObservationStoreException; -import org.eclipse.californium.elements.EndpointContext; import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Stoppable; @@ -30,8 +30,8 @@ import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.peer.LwM2mIdentity; import org.eclipse.leshan.core.util.NamedThreadFactory; import org.eclipse.leshan.core.util.Validate; -import org.eclipse.leshan.server.californium.observation.ObservationSerDes; import org.eclipse.leshan.server.redis.RedisRegistrationStore; +import org.eclipse.leshan.server.redis.serialization.ObservationSerDes; import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes; import org.eclipse.leshan.server.registration.Deregistration; import org.eclipse.leshan.server.registration.ExpirationListener; @@ -65,6 +65,7 @@ import java.util.concurrent.locks.Lock; import static java.nio.charset.StandardCharsets.UTF_8; +@Slf4j public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startable, Stoppable, Destroyable { /** Default time in seconds between 2 cleaning tasks (used to remove expired registration). */ public static final long DEFAULT_CLEAN_PERIOD = 60; @@ -86,7 +87,9 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab // (expiration date, Endpoint) private final RegistrationSerDes registrationSerDes = new RegistrationSerDes(); - private final ObservationSerDes observationSerDes = new ObservationSerDes(new UdpDataParser(), new UdpDataSerializer()); + private final ObservationSerDes observationSerDes = new ObservationSerDes(); + private final org.eclipse.leshan.server.californium.observation.ObservationSerDes observationSerDesCoap = + new org.eclipse.leshan.server.californium.observation.ObservationSerDes(new UdpDataParser(), new UdpDataSerializer()); private final RedisConnectionFactory connectionFactory; // Listener use to notify when a registration expires @@ -255,6 +258,18 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab return getRegistration(connection, registrationId); } } + private Registration getRegistration(RedisConnection connection, String registrationId) { + byte[] ep = connection.get(toRegIdKey(registrationId)); + if (ep == null) { + return null; + } + byte[] data = connection.get(toEndpointKey(ep)); + if (data == null) { + return null; + } + + return deserializeReg(data); + } @Override public Registration getRegistrationByEndpoint(String endpoint) { @@ -331,63 +346,6 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab } } - @Override - public Collection addObservation(String registrationId, Observation observation, boolean addIfAbsent) { - List removed = new ArrayList<>(); - try (var connection = connectionFactory.getConnection()) { - - // fetch the client ep by registration ID index - byte[] ep = connection.get(toRegIdKey(registrationId)); - if (ep == null) { - return null; - } - - Lock lock = null; - String lockKey = toLockKey(ep); - - try { - lock = redisLock.obtain(lockKey); - lock.lock(); - - // cancel existing observations for the same path and registration id. - for (Observation obs : getObservations(connection, registrationId)) { - //TODO: should be able to use CompositeObservation - if (((SingleObservation)observation).getPath().equals(((SingleObservation)obs).getPath()) - && !observation.getId().equals(obs.getId())) { - removed.add(obs); - unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); - } - } - - } finally { - if (lock != null) { - lock.unlock(); - } - } - } - return removed; - } - - @Override - public Collection getObservations(String registrationId) { - try (var connection = connectionFactory.getConnection()) { - return getObservations(connection, registrationId); - } - } - @Override - public Observation getObservation(String registrationId, ObservationIdentifier observationId) { - return getObservations(registrationId).stream().filter(o -> o.getId()==observationId).findFirst().get(); - } - - @Override - public Observation getObservation(ObservationIdentifier observationId) { - return null; - } - - @Override - public Observation removeObservation(String registrationId, ObservationIdentifier observationId) { - return removeObservation(registrationId, observationId.getBytes()); - } private Deregistration removeRegistration(RedisConnection connection, String registrationId, boolean removeOnlyIfNotAlive) { // fetch the client ep by registration ID index @@ -499,6 +457,91 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab * org.eclipse.californium.core.observe.ObservationStore#add method) */ + @Override + public Collection addObservation(String registrationId, Observation observation, boolean addIfAbsent) { + List removed = new ArrayList<>(); + try (var connection = connectionFactory.getConnection()) { + + // fetch the client ep by registration ID index + byte[] ep = connection.get(toRegIdKey(registrationId)); + if (ep == null) { + throw new IllegalStateException(String.format( + "can not add observation %s there is no registration with id %s", observation, registrationId)); + } + + Lock lock = null; + String lockKey = toLockKey(ep); + + try { + lock = redisLock.obtain(lockKey); + lock.lock(); + + // Add and Get previous observation + byte[] previousValue; + byte[] key = toKey(OBS_TKN, observation.getId().getBytes()); + byte[] serializeObs = serializeObs(observation); + if (addIfAbsent) { + previousValue = connection.get(key); + if (previousValue == null || previousValue.length == 0) { + connection.set(key, serializeObs); + } + } else { + previousValue = connection.getSet(key, serializeObs); + } + + // secondary index to get the list by registrationId + connection.lPush(toKey(OBS_TKNS_REGID_IDX, registrationId), observation.getId().getBytes()); + + // log any collisions + Observation previousObservation; + if (previousValue != null && previousValue.length != 0) { + previousObservation = deserializeObs(previousValue); + LOG.warn("Token collision ? observation [{}] will be replaced by observation [{}] ", + previousObservation, observation); + } + // cancel existing observations for the same path and registration id. + for (Observation obs : getObservations(connection, registrationId)) { + //TODO: should be able to use CompositeObservation + if (((SingleObservation)observation).getPath().equals(((SingleObservation)obs).getPath()) + && !observation.getId().equals(obs.getId())) { + removed.add(obs); + unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); + } + } + } finally { + if (lock != null) { + lock.unlock(); + } + } + } + return removed; + } + + @Override + public Collection getObservations(String registrationId) { + try (var connection = connectionFactory.getConnection()) { + return getObservations(connection, registrationId); + } + } + @Override + public Observation getObservation(String registrationId, ObservationIdentifier observationId) { + return getObservations(registrationId).stream().filter( + o -> o.getId().getAsHexString().equals(observationId.getAsHexString())).findFirst().get(); + } + + @Override + public Observation getObservation(ObservationIdentifier observationId) { + try (var connection = connectionFactory.getConnection()) { + byte[] observationValue = connection.get(toKey(OBS_TKN, observationId.getBytes())); + return deserializeObs(observationValue); + } + } + + @Override + public Observation removeObservation(String registrationId, ObservationIdentifier observationId) { + return removeObservation(registrationId, observationId.getBytes()); + } + public Observation removeObservation(String registrationId, byte[] observationId) { try (var connection = connectionFactory.getConnection()) { @@ -515,8 +558,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab lock = redisLock.obtain(lockKey); lock.lock(); -// Observation observation = build(get(new Token(observationId))); - Observation observation = build(null); + Observation observation = get(new Token(observationId)); if (observation != null && registrationId.equals(observation.getRegistrationId())) { unsafeRemoveObservation(connection, registrationId, observationId); return observation; @@ -536,7 +578,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab for (byte[] token : connection.lRange(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, -1)) { byte[] obs = connection.get(toKey(OBS_TKN, token)); if (obs != null) { - result.add(build(deserializeObs(obs))); + result.add(deserializeObs(obs)); } } return result; @@ -593,15 +635,16 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab if (!connection.exists(toRegIdKey(registrationId))) throw new ObservationStoreException("no registration for this Id"); byte[] key = toKey(OBS_TKN, obs.getRequest().getToken().getBytes()); - byte[] serializeObs = serializeObs(obs); + Observation obsLeshan = buildLwM2mObservationFromCfToLeshanCore(obs); + byte[] serializeObs = serializeObs(obsLeshan); byte[] previousValue; if (ifAbsent) { previousValue = connection.get(key); if (previousValue == null || previousValue.length == 0) { connection.set(key, serializeObs); + previousValue = connection.getSet(key, serializeObs); } else { -// return deserializeObs(previousValue); - return null; + return buildCoapObservationFromLeshanCoreToCfCore(deserializeObs(previousValue)); } } else { previousValue = connection.getSet(key, serializeObs); @@ -612,8 +655,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab // log any collisions if (previousValue != null && previousValue.length != 0) { -// previousObservation = deserializeObs(previousValue); - previousObservation = null; + previousObservation = buildCoapObservationFromLeshanCoreToCfCore(deserializeObs(previousValue)); LOG.warn( "Token collision ? observation from request [{}] will be replaced by observation from request [{}] ", previousObservation.getRequest(), obs.getRequest()); @@ -663,33 +705,19 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab } - public org.eclipse.californium.core.observe.Observation get(Token token) { + public Observation get(Token token) { try (var connection = connectionFactory.getConnection()) { byte[] obs = connection.get(toKey(OBS_TKN, token.getBytes())); if (obs == null) { return null; } else { -// return deserializeObs(obs); - return null; + return deserializeObs(obs); } } } /* *************** Observation utility functions **************** */ - private Registration getRegistration(RedisConnection connection, String registrationId) { - byte[] ep = connection.get(toRegIdKey(registrationId)); - if (ep == null) { - return null; - } - byte[] data = connection.get(toEndpointKey(ep)); - if (data == null) { - return null; - } - - return deserializeReg(data); - } - private void unsafeRemoveObservation(RedisConnection connection, String registrationId, byte[] observationId) { if (connection.del(toKey(OBS_TKN, observationId)) > 0L) { connection.lRem(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, observationId); @@ -704,7 +732,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab for (byte[] token : connection.lRange(regIdKey, 0, -1)) { byte[] obs = connection.get(toKey(OBS_TKN, token)); if (obs != null) { - removed.add(build(deserializeObs(obs))); + removed.add(deserializeObs(obs)); } connection.del(toKey(OBS_TKN, token)); } @@ -713,26 +741,22 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab return removed; } - public void setContext(Token token, EndpointContext correlationContext) { - // In Leshan we always set context when we send the request, so this should not be needed to implement this. + private byte[] serializeObs(Observation obs) { + return observationSerDes.serialize(obs); } - private byte[] serializeObs(org.eclipse.californium.core.observe.Observation obs) { - return null; -// return observationSerDes.serialize(obs); + private org.eclipse.leshan.core.observation.Observation buildLwM2mObservationFromCfToLeshanCore( + org.eclipse.californium.core.observe.Observation observation) { + String serializedObservation = observationSerDesCoap.serialize(observation); + return serializedObservation == null ? null : ObserveUtil.createLwM2mObservation(observation, serializedObservation); } - - private Observation deserializeObs(byte[] data) { -// return observationSerDes.deserialize(data); - return null; + private org.eclipse.californium.core.observe.Observation buildCoapObservationFromLeshanCoreToCfCore(Observation obs) { + String serializedObservation = ObserveUtil.extractSerializedObservation(obs); + return serializedObservation == null ? null : observationSerDesCoap.deserialize(serializedObservation); } - private Observation build(Observation cfObs) { - if (cfObs == null) - return null; -// String serializedObservation = observationSerDes.serialize(cfObs); -// return ObserveUtil.createLwM2mObservation(cfObs, serializedObservation); - return null; + private Observation deserializeObs(byte[] data) { + return data == null ? null : observationSerDes.deserialize(data); } /* *************** Expiration handling **************** */ From e03005d780e93a5477118ba6f46662d0da14dc4a Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 16 Jan 2024 18:44:20 +0200 Subject: [PATCH 09/23] lwm2m: add observation, composite observation, cancel observation --- .../observation/ObservationServiceImpl.java | 281 +++++++++ .../server/edge/DeviceProfileEdgeTest.java | 12 +- .../lwm2m/AbstractLwM2MIntegrationTest.java | 16 +- .../transport/lwm2m/Lwm2mTestHelper.java | 2 + .../lwm2m/client/SimpleLwM2MDevice.java | 3 +- .../ota/AbstractOtaLwM2MIntegrationTest.java | 41 ++ .../ota/sql/OtaLwM2MIntegrationTest.java | 40 -- ...bstractRpcLwM2MIntegrationObserveTest.java | 34 ++ ...cLwm2MIntegrationObserveCompositeTest.java | 446 ++++++++++++++ .../sql/RpcLwm2mIntegrationDiscoverTest.java | 5 +- .../sql/RpcLwm2mIntegrationObserveTest.java | 283 +++++++-- application/src/test/resources/lwm2m/5.xml | 2 +- application/src/test/resources/lwm2m/9.xml | 2 +- common/transport/lwm2m/pom.xml | 2 +- .../LwM2MTransportBootstrapService.java | 1 + ...LwM2MBootstrapConfigStoreTaskProvider.java | 8 +- .../config/LwM2MTransportServerConfig.java | 5 + .../lwm2m/server/LwM2mServerListener.java | 21 +- .../lwm2m/server/LwM2mSessionMsgListener.java | 2 +- .../server/LwM2mTransportServerHelper.java | 6 +- .../DefaultLwM2mDownlinkMsgHandler.java | 111 +++- .../downlink/LwM2mDownlinkMsgHandler.java | 10 +- ...TbLwM2MCancelObserveCompositeCallback.java | 40 ++ .../TbLwM2MCancelObserveCompositeRequest.java | 32 + .../TbLwM2MObserveCompositeCallback.java | 39 ++ .../TbLwM2MObserveCompositeRequest.java | 51 ++ .../ota/DefaultLwM2MOtaUpdateService.java | 55 +- .../rpc/DefaultLwM2MRpcRequestHandler.java | 33 +- .../RpcCancelObserveCompositeCallback.java | 37 ++ .../RpcObserveResponseCompositeCallback.java | 40 ++ .../store/TbInMemoryRegistrationStore.java | 577 ++++++++++++++++++ .../store/TbLwM2mRedisRegistrationStore.java | 326 ++++++---- .../server/store/TbLwM2mStoreFactory.java | 7 +- .../uplink/DefaultLwM2mUplinkMsgHandler.java | 29 +- .../server/uplink/LwM2mUplinkMsgHandler.java | 1 + .../lwm2m/utils/LwM2MTransportUtil.java | 94 +-- 36 files changed, 2305 insertions(+), 389 deletions(-) create mode 100644 application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.java create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java diff --git a/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java b/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java new file mode 100644 index 0000000000..571b0e09bd --- /dev/null +++ b/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java @@ -0,0 +1,281 @@ +/** + * 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.eclipse.leshan.server.observation; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.peer.LwM2mPeer; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; +import org.eclipse.leshan.core.response.ObserveResponse; +import org.eclipse.leshan.server.endpoint.LwM2mServerEndpoint; +import org.eclipse.leshan.server.endpoint.LwM2mServerEndpointsProvider; +import org.eclipse.leshan.server.profile.ClientProfile; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.registration.RegistrationStore; +import org.eclipse.leshan.server.registration.RegistrationUpdate; +import org.eclipse.leshan.server.registration.UpdatedRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Implementation of the {@link ObservationService} accessing the persisted observation via the provided + * {@link RegistrationStore}. + * + * When a new observation is added or changed or canceled, the registered listeners are notified. + */ +@Slf4j +public class ObservationServiceImpl implements ObservationService, LwM2mNotificationReceiver { + + private final Logger LOG = LoggerFactory.getLogger(ObservationServiceImpl.class); + + private final RegistrationStore registrationStore; + private final LwM2mServerEndpointsProvider endpointProvider; + private final boolean updateRegistrationOnNotification; + + private final List listeners = new CopyOnWriteArrayList<>();; + + /** + * Creates an instance of {@link ObservationServiceImpl} + */ + public ObservationServiceImpl(RegistrationStore store, LwM2mServerEndpointsProvider endpointProvider) { + this(store, endpointProvider, false); + } + + /** + * Creates an instance of {@link ObservationServiceImpl} + * + * @param updateRegistrationOnNotification will activate registration update on observe notification. + * + * @since 1.1 + */ + public ObservationServiceImpl(RegistrationStore store, LwM2mServerEndpointsProvider endpointProvider, + boolean updateRegistrationOnNotification) { + this.registrationStore = store; + this.updateRegistrationOnNotification = updateRegistrationOnNotification; + this.endpointProvider = endpointProvider; + } + + @Override + public int cancelObservations(Registration registration) { + // check registration id + String registrationId = registration.getId(); + if (registrationId == null) + return 0; + + Collection observations = registrationStore.removeObservations(registrationId); + if (observations == null) + return 0; + + for (Observation observation : observations) { + cancel(observation); + } + + return observations.size(); + } + + @Override + public int cancelObservations(Registration registration, String nodePath) { + if (registration == null || registration.getId() == null || nodePath == null || nodePath.isEmpty()) + return 0; + + Set observations = getObservationsForCancel(registration.getId(), nodePath); + for (Observation observation : observations) { + cancelObservation(observation); + } + return observations.size(); + } + + @Override + public int cancelCompositeObservations(Registration registration, String[] nodePaths) { + if (registration == null || registration.getId() == null || nodePaths == null || nodePaths.length == 0) + return 0; + + Set observations = getCompositeObservationsForCancel(registration.getId(), nodePaths); + for (Observation observation : observations) { + cancelObservation(observation); + } + return observations.size(); + } + + @Override + public void cancelObservation(Observation observation) { + if (observation == null) + return; + + registrationStore.removeObservation(observation.getRegistrationId(), observation.getId()); + cancel(observation); + } + + private void cancel(Observation observation) { + List endpoints = endpointProvider.getEndpoints(); + for (LwM2mServerEndpoint lwM2mEndpoint : endpoints) { + lwM2mEndpoint.cancelObservation(observation); + } + + for (ObservationListener listener : listeners) { + listener.cancelled(observation); + } + } + + @Override + public Set getObservations(Registration registration) { + return getObservations(registration.getId()); + } + + private Set getObservations(String registrationId) { + if (registrationId == null) + return Collections.emptySet(); + + return new HashSet<>(registrationStore.getObservations(registrationId)); + } + + private Set getCompositeObservationsForCancel(String registrationId, String[] nodePaths) { + if (registrationId == null || nodePaths == null) + return Collections.emptySet(); + + // array of String to array of LWM2M path + List lwPaths = new ArrayList<>(nodePaths.length); + for (int i = 0; i < nodePaths.length; i++) { + lwPaths.add(new LwM2mPath(nodePaths[i])); + } + + // search composite-observation + Set result = new HashSet<>(); + for (Observation obs : getObservations(registrationId)) { + if (obs instanceof CompositeObservation) { + if (lwPaths.equals(((CompositeObservation) obs).getPaths())) { + result.add(obs); + } + } + } + return result; + } + + private Set getObservationsForCancel(String registrationId, String nodePath) { + if (registrationId == null || nodePath == null) + return Collections.emptySet(); + + Set result = new HashSet<>(); + LwM2mPath lwPath = new LwM2mPath(nodePath); + for (Observation obs : getObservations(registrationId)) { + if (obs instanceof SingleObservation) { + LwM2mPath lwPathObs = ((SingleObservation) obs).getPath(); + if (lwPath.equals(lwPathObs) || lwPathObs.startWith(lwPath)) { // nodePath = "3", lwPathObs = "3/0/9": cancel for tne all lwPathObs + result.add(obs); + } else if (!lwPath.equals(lwPathObs) && lwPath.startWith(lwPathObs)) { // nodePath = "3/0/9", lwPathObs = "3": error... + String errorMsg = String.format( + "Unexpected error: There is registration with id %s for observation path %s, that includes this observation path %s", + registrationId, lwPath, lwPathObs); + throw new IllegalStateException(errorMsg); + } + } + } + + return result; + } + + @Override + public void addListener(ObservationListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(ObservationListener listener) { + listeners.remove(listener); + } + + private Registration updateRegistrationOnRegistration(Observation observation, LwM2mPeer sender, + ClientProfile profile) { + if (updateRegistrationOnNotification) { + RegistrationUpdate regUpdate = new RegistrationUpdate(observation.getRegistrationId(), sender, null, null, + null, null, null, null, null, null, null, null); + UpdatedRegistration updatedRegistration = registrationStore.updateRegistration(regUpdate); + if (updatedRegistration == null || updatedRegistration.getUpdatedRegistration() == null) { + String errorMsg = String.format( + "Unexpected error: There is no registration with id %s for this observation %s", + observation.getRegistrationId(), observation); + LOG.error(errorMsg); + throw new IllegalStateException(errorMsg); + } + return updatedRegistration.getUpdatedRegistration(); + } + return profile.getRegistration(); + } + + // ********** NotificationListener interface **********// + @Override + public void onNotification(SingleObservation observation, LwM2mPeer sender, ClientProfile profile, + ObserveResponse response) { + try { + Registration updatedRegistration = updateRegistrationOnRegistration(observation, sender, profile); + for (ObservationListener listener : listeners) { + listener.onResponse(observation, updatedRegistration, response); + } + } catch (Exception e) { + for (ObservationListener listener : listeners) { + listener.onError(observation, profile.getRegistration(), e); + } + } + } + + @Override + public void onNotification(CompositeObservation observation, LwM2mPeer sender, ClientProfile profile, + ObserveCompositeResponse response) { + try { + Registration updatedRegistration = updateRegistrationOnRegistration(observation, sender, profile); + for (ObservationListener listener : listeners) { + listener.onResponse(observation, updatedRegistration, response); + } + } catch (Exception e) { + for (ObservationListener listener : listeners) { + listener.onError(observation, profile.getRegistration(), e); + } + } + } + + @Override + public void onError(Observation observation, LwM2mPeer sender, ClientProfile profile, Exception error) { + for (ObservationListener listener : listeners) { + listener.onError(observation, profile.getRegistration(), error); + } + } + + @Override + public void newObservation(Observation observation, Registration registration) { + for (ObservationListener listener : listeners) { + listener.newObservation(observation, registration); + } + } + + @Override + public void cancelled(Observation observation) { + for (ObservationListener listener : listeners) { + listener.cancelled(observation); + } + + } +} diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java index 97c4b0b333..fc877ae6f8 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java @@ -198,16 +198,16 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { Assert.assertEquals("PUBLIC_KEY", noSecLwM2MBootstrapServerCredential.getServerPublicKey()); Assert.assertEquals(Integer.valueOf(123), noSecLwM2MBootstrapServerCredential.getShortServerId()); - Assert.assertTrue(noSecLwM2MBootstrapServerCredential.isBootstrapServerIs()); + Assert.assertFalse(noSecLwM2MBootstrapServerCredential.isBootstrapServerIs()); Assert.assertEquals("localhost", noSecLwM2MBootstrapServerCredential.getHost()); - Assert.assertEquals(Integer.valueOf(5687), noSecLwM2MBootstrapServerCredential.getPort()); + Assert.assertEquals(Integer.valueOf(5685), noSecLwM2MBootstrapServerCredential.getPort()); TelemetryMappingConfiguration observeAttr = transportConfiguration.getObserveAttr(); - Assert.assertEquals("batteryLevel", observeAttr.getKeyName().get("/3_1.0/0/9")); + Assert.assertEquals("batteryLevel", observeAttr.getKeyName().get("/3_1.2/0/9")); Assert.assertTrue(observeAttr.getObserve().isEmpty()); Assert.assertTrue(observeAttr.getAttribute().isEmpty()); Assert.assertFalse(observeAttr.getTelemetry().isEmpty()); - Assert.assertTrue(observeAttr.getTelemetry().contains("/3_1.0/0/9")); + Assert.assertTrue(observeAttr.getTelemetry().contains("/3_1.2/0/9")); Assert.assertTrue(observeAttr.getAttributeLwm2m().isEmpty()); removeDeviceProfileAndDoBasicAssert(deviceProfile); @@ -366,9 +366,9 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { AbstractLwM2MBootstrapServerCredential bootstrapServerCredential = new NoSecLwM2MBootstrapServerCredential(); bootstrapServerCredential.setServerPublicKey("PUBLIC_KEY"); bootstrapServerCredential.setShortServerId(123); - bootstrapServerCredential.setBootstrapServerIs(true); + bootstrapServerCredential.setBootstrapServerIs(false); bootstrapServerCredential.setHost("localhost"); - bootstrapServerCredential.setPort(5687); + bootstrapServerCredential.setPort(5685); bootstrap.add(bootstrapServerCredential); transportConfiguration.setBootstrap(bootstrap); 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 9cd6a66cb5..267accc51c 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 @@ -145,8 +145,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte " \"telemetry\": [],\n" + " \"attributeLwm2m\": {}\n" + " }"; - - public static final String OBSERVE_ATTRIBUTES_WITH_PARAMS = + public static String OBSERVE_ATTRIBUTES_WITH_PARAMS = " {\n" + " \"keyName\": {\n" + @@ -403,17 +402,24 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte } protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception { - await("ObserveReadAll after start client: countObserve " + cntObserve) + await("ObserveReadAll after start client/test: countObserve " + cntObserve) .atMost(40, TimeUnit.SECONDS) .until(() -> cntObserve == getCntObserveAll(deviceIdStr)); } - protected int getCntObserveAll(String deviceIdStr) throws Exception { + protected Integer getCntObserveAll(String deviceIdStr) throws Exception { String actualResultBefore = sendObserve("ObserveReadAll", null, deviceIdStr); ObjectNode rpcActualResultBefore = JacksonUtil.fromString(actualResultBefore, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultBefore.get("result").asText()); JsonElement element = JsonUtils.parse(rpcActualResultBefore.get("value").asText()); - return element.isJsonArray() ? ((JsonArray)element).size() : 0; + return element.isJsonArray() ? ((JsonArray)element).size() : null; + } + + protected void sendCancelObserveAllWithAwait(String deviceIdStr) throws Exception { + String actualResultCancelAll = sendObserve("ObserveCancelAll", null, deviceIdStr); + ObjectNode rpcActualResultCancelAll = JacksonUtil.fromString(actualResultCancelAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultCancelAll.get("result").asText()); + awaitObserveReadAll(0, deviceId); } protected String sendObserve(String method, String params, String deviceIdStr) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 6eb8dab73f..7b80b677cc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -34,6 +34,7 @@ public class Lwm2mTestHelper { public static final int RESOURCE_ID_2 = 2; public static final int RESOURCE_ID_3 = 3; public static final int RESOURCE_ID_4 = 4; + public static final int RESOURCE_ID_5 = 5; public static final int RESOURCE_ID_6 = 6; public static final int RESOURCE_ID_7 = 7; public static final int RESOURCE_ID_8 = 8; @@ -41,6 +42,7 @@ public class Lwm2mTestHelper { public static final int RESOURCE_ID_11 = 11; public static final int RESOURCE_ID_14 = 14; public static final int RESOURCE_ID_15 = 15; + public static final int RESOURCE_INSTANCE_ID_0 = 0; public static final int RESOURCE_INSTANCE_ID_2 = 2; public static final String RESOURCE_ID_NAME_3_9 = "batteryLevel"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java index 89833a43a6..e6fe6598aa 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java @@ -58,7 +58,8 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl executorService.scheduleWithFixedDelay(() -> { fireResourceChange(9); } - , 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN + , 1, 1, TimeUnit.SECONDS); // 30 MIN +// , 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN } catch (Throwable e) { log.error("[{}]Throwable", e.toString()); e.printStackTrace(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java index 6b9bb2ec02..7cbd7af5a7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java @@ -34,6 +34,47 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg protected static final String CLIENT_ENDPOINT_OTA5 = "Ota5_Device"; protected static final String CLIENT_ENDPOINT_OTA9 = "Ota9_Device"; + + protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA = + + " {\n" + + " \"keyName\": {\n" + + " \"/5_1.2/0/3\": \"state\",\n" + + " \"/5_1.2/0/5\": \"updateResult\",\n" + + " \"/5_1.2/0/6\": \"pkgname\",\n" + + " \"/5_1.2/0/7\": \"pkgversion\",\n" + + " \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\",\n" + + " \"/9_1.1/0/0\": \"pkgname\",\n" + + " \"/9_1.1/0/1\": \"pkgversion\",\n" + + " \"/9_1.1/0/7\": \"updateState\",\n" + + " \"/9_1.1/0/9\": \"updateResult\"\n" + + " },\n" + + " \"observe\": [\n" + + " \"/5_1.2/0/3\",\n" + + " \"/5_1.2/0/5\",\n" + + " \"/5_1.2/0/6\",\n" + + " \"/5_1.2/0/7\",\n" + + " \"/5_1.2/0/9\",\n" + + " \"/9_1.1/0/0\",\n" + + " \"/9_1.1/0/1\",\n" + + " \"/9_1.1/0/7\",\n" + + " \"/9_1.1/0/9\"\n" + + " ],\n" + + " \"attribute\": [],\n" + + " \"telemetry\": [\n" + + " \"/5_1.2/0/3\",\n" + + " \"/5_1.2/0/5\",\n" + + " \"/5_1.2/0/6\",\n" + + " \"/5_1.2/0/7\",\n" + + " \"/5_1.2/0/9\",\n" + + " \"/9_1.1/0/0\",\n" + + " \"/9_1.1/0/1\",\n" + + " \"/9_1.1/0/7\",\n" + + " \"/9_1.1/0/9\"\n" + + " ],\n" + + " \"attributeLwm2m\": {}\n" + + " }"; + public AbstractOtaLwM2MIntegrationTest() { setResources(this.RESOURCES_OTA); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index 6e6b0f2c2a..fed02cd8ca 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java @@ -51,46 +51,6 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfil @Slf4j public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { - protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA = - - " {\n" + - " \"keyName\": {\n" + - " \"/5_1.0/0/3\": \"state\",\n" + - " \"/5_1.0/0/5\": \"updateResult\",\n" + - " \"/5_1.0/0/6\": \"pkgname\",\n" + - " \"/5_1.0/0/7\": \"pkgversion\",\n" + - " \"/5_1.0/0/9\": \"firmwareUpdateDeliveryMethod\",\n" + - " \"/9_1.0/0/0\": \"pkgname\",\n" + - " \"/9_1.0/0/1\": \"pkgversion\",\n" + - " \"/9_1.0/0/7\": \"updateState\",\n" + - " \"/9_1.0/0/9\": \"updateResult\"\n" + - " },\n" + - " \"observe\": [\n" + - " \"/5_1.0/0/3\",\n" + - " \"/5_1.0/0/5\",\n" + - " \"/5_1.0/0/6\",\n" + - " \"/5_1.0/0/7\",\n" + - " \"/5_1.0/0/9\",\n" + - " \"/9_1.0/0/0\",\n" + - " \"/9_1.0/0/1\",\n" + - " \"/9_1.0/0/7\",\n" + - " \"/9_1.0/0/9\"\n" + - " ],\n" + - " \"attribute\": [],\n" + - " \"telemetry\": [\n" + - " \"/5_1.0/0/3\",\n" + - " \"/5_1.0/0/5\",\n" + - " \"/5_1.0/0/6\",\n" + - " \"/5_1.0/0/7\",\n" + - " \"/5_1.0/0/9\",\n" + - " \"/9_1.0/0/0\",\n" + - " \"/9_1.0/0/1\",\n" + - " \"/9_1.0/0/7\",\n" + - " \"/9_1.0/0/9\"\n" + - " ],\n" + - " \"attributeLwm2m\": {}\n" + - " }"; - private List expectedStatuses; @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java new file mode 100644 index 0000000000..63558fd489 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java @@ -0,0 +1,34 @@ +/** + * 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.lwm2m.rpc; + +import org.junit.Before; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public abstract class AbstractRpcLwM2MIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest{ + private final String[] RESOURCES_RPC_MULTIPLE_19 = new String[]{"0.xml", "1.xml", "2.xml", "3.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; + + public AbstractRpcLwM2MIntegrationObserveTest() { + setResources(this.RESOURCES_RPC_MULTIPLE_19); + } + + @Before + public void initTest () throws Exception { + awaitObserveReadAll(2, deviceId); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java new file mode 100644 index 0000000000..ed7d71caee --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java @@ -0,0 +1,446 @@ +/** + * 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.lwm2m.rpc.sql; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.eclipse.leshan.core.ResponseCode; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_15; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_5; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; + +public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MIntegrationObserveTest { + + + /** + * ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9", "19/1/0/0"]} - Ok + * @throws Exception + */ + @Test + public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mResourceInstance() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; + String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; + String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; + String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; + String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; + String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actualValues = rpcActualResult.get("value").asText(); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_7) + "=LwM2mSingleResource")); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_3) + "=LwM2mSingleResource")); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_5) + "=LwM2mSingleResource")); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(idVer_3_0_9) + "=LwM2mSingleResource")); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0_0) + "=LwM2mResourceInstance")); + } + + /** + * ObserveComposite {"ids":["19/1/0/0", "5/0"]} - Ok + * @throws Exception + */ + @Test + public void testObserveComposite_ObjectInstanceWithOtherObjectResourceInstance_Result_CONTENT_Ok() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; + String expectedIdVer5_0 = objectInstanceIdVer_5; + String expectedIds = "[\"" + expectedIdVer19_1_0 + "\", \"" + expectedIdVer5_0 + "\"]"; + String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actual= rpcActualResult.get("value").asText(); + assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0) + "=LwM2mMultipleResource")); + assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer5_0) + "=LwM2mObjectInstance")); + } + + /** + * ObserveComposite {"ids":["5/0/7", "5/0/2"]} - Ok + * "5/0/2" - Execute^ result == null + * @throws Exception + */ + @Test + public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_If_Error_Null() throws Exception { +// sendCancelObserveAllWithAwait(deviceId); + String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; + String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; + String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_2 + "\"]"; + String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actualValues = rpcActualResult.get("value").asText(); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_7) + "=LwM2mSingleResource")); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_2) + "=null")); + } + + + /** + * ObserveComposite {"ids":["5/0/7", "5/0/2"]} - Ok + * "5/0" contains "5/0/2" + * @throws Exception + */ + @Test + public void testObserveComposite_Result_BAD_REQUEST_ONE_PATH_CONTAINCE_OTHER() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + String expectedIdVer5_0 = objectInstanceIdVer_5; + String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; + String expectedIds = "[\"" + expectedIdVer5_0 + "\", \"" + expectedIdVer5_0_2 + "\"]"; + String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); + String actual= rpcActualResult.get("error").asText(); + String expected = "Invalid path list : /5/0 and /5/0/2 are overlapped paths"; + assertTrue(expected.equals(actual)); + } + + /** + * Previous -> "3/0/9" + * ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9"]} - CONTENT + * @throws Exception + */ + @Test + public void testObserveCompositeThereAreObservationOneResource_Result_CONTENT_Value_ObservationAddIfAbsent() throws Exception { + String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; + String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; + String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; + String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\"]"; + String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String expectedResult = "/3/0/9=LwM2mSingleResource [id=9"; + assertTrue(rpcActualResult.get("value").asText().contains(expectedResult)); + } + + /** + * ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9", "19/1/0"]} - Ok + * @throws Exception + */ + @Test + public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mMultipleResource() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + + String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; + String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; + String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; + String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; + String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0 + "\"]"; + String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actualValues = rpcActualResult.get("value").asText(); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_7) + "=LwM2mSingleResource")); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_3) + "=LwM2mSingleResource")); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_5 + "=LwM2mSingleResource"))); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(idVer_3_0_9) + "=LwM2mSingleResource")); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0) + "=LwM2mMultipleResource")); + } + + /** + * ObserveComposite with keyName {"keys":["batteryLevel", "UtfOffset", "dataRead", "dataWrite"]} - Ok + * @throws Exception + */ + @Test + public void testObserveCompositeWithKeyName_Result_CONTENT_Value_SingleResources() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + + String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; + String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; + String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0; + String expectedKey19_1_0 = RESOURCE_ID_NAME_19_1_0; + String expectedKeys = "[\"" + expectedKey3_0_9 + "\", \"" + expectedKey3_0_14 + "\", \"" + expectedKey19_0_0 + "\", \"" + expectedKey19_1_0 + "\"]"; + String actualResult = sendCompositeRPCByKeys("ObserveComposite", expectedKeys); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actualValues = rpcActualResult.get("value").asText(); + String expectedIdVer3_0_14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; + String expectedIdVer19_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; + String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer3_0_14))); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer19_0_0))); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0))); + assertTrue(actualValues.contains(fromVersionedIdToObjectId(idVer_3_0_9))); + } + + /** + * ObserveComposite with keyName {"keys":["batteryLevel", "UtfOffset", "dataRead", "dataWrite"]} - - BAD_REQUEST + * @throws Exception + */ + @Test + public void testObserveCompositeWithKeyNameThereAreObservationOneResource_Result_CONTENT_Value_ObservationAddIfAbsent() throws Exception { + String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; + String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; + String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0; + String expectedKey19_1_0 = RESOURCE_ID_NAME_19_1_0; + String expectedKeys = "[\"" + expectedKey3_0_9 + "\", \"" + expectedKey3_0_14 + "\", \"" + expectedKey19_0_0 + "\", \"" + expectedKey19_1_0 + "\"]"; + String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; + String actualResult = sendCompositeRPCByKeys("ObserveComposite", expectedKeys); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actual = rpcActualResult.get("value").asText(); + assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0) + "=LwM2mMultipleResource")); + } + + /** + * ObserveReadAll + * {"result":"CONTENT","value":"[\"CompositeObservation: [/19/1/0\",\"/19/0/0\",\"/3/0/14\",\"/3/0/9]\"]"} - Ok + * @throws Exception + */ + @Test + public void testObserveReadAll_AfterCompositeObservation_Result_CONTENT_Value_SingleObservation_Only() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + + String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; + String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; + String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0; + String expectedKey19_1_0 = RESOURCE_ID_NAME_19_1_0; + String expectedKeys = "[\"" + expectedKey3_0_9 + "\", \"" + expectedKey3_0_14 + "\", \"" + expectedKey19_0_0 + "\", \"" + expectedKey19_1_0 + "\"]"; + String actualResult = sendCompositeRPCByKeys("ObserveComposite", expectedKeys); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); + ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); + String actualValues = rpcActualResultReadAll.get("value").asText(); + String expectedIdVer3_0_14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; + String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; + assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9))); + assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer3_0_14))); + assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0))); + assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0))); + } + + /** + * ObserveReadAll + * {"result":"CONTENT","value":"{"result":"CONTENT","value":"["SingleObservation:/3/0/9","SingleObservation:/3/0/14","SingleObservation:/19/1/0/0","SingleObservation:/19/0/0"]"} - Ok + * @throws Exception + */ + @Test + public void testObserveReadAll_Result_CONTENT_Value_SingleObservation_Only() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + + String expectedIdVer3_0_14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; + String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; + String actualResult3_0_9 = sendObserve("Observe", idVer_3_0_9); + ObjectNode rpcActualResult3_0_9 = JacksonUtil.fromString(actualResult3_0_9, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult3_0_9.get("result").asText()); + String actualResult3_0_14 = sendObserve("Observe", expectedIdVer3_0_14); + ObjectNode rpcActualResult3_0_14 = JacksonUtil.fromString(actualResult3_0_14, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult3_0_14.get("result").asText()); + String actualResult19_1_0_0 = sendObserve("Observe", expectedIdVer19_1_0_0); + ObjectNode rpcActualResult19_1_0_0 = JacksonUtil.fromString(actualResult19_1_0_0, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult19_1_0_0.get("result").asText()); + String actualResult19_0_0 = sendObserve("Observe", idVer_19_0_0); + ObjectNode rpcActualResult19_0_0 = JacksonUtil.fromString(actualResult19_0_0, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult19_0_0.get("result").asText()); + String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); + ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); + String actualValues = rpcActualResultReadAll.get("value").asText(); + assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9))); + assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer3_0_14))); + assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0_0))); + assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0))); + } + + /** + * ObserveReadAll + * {"result":"CONTENT","value":"[\"CompositeObservation: [/19/1/0\",\"/19/0/0\",\"/3/0/14\",\"/3/0/9]\"]"} - Ok + * @throws Exception + */ + @Test + public void testObserveReadAll_AfterCompositeObservation_WithResourceNotReadable_Result_CONTENT_Value_SingleObservation_Only() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + + String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; + String expectedIdVer5_0_2= objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; + String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; + String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; + String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_2 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0 + "\"]"; + String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actualValues = rpcActualResult.get("value").asText(); + + assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_2) + "=null")); + + String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); + ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); + actualValues = rpcActualResultReadAll.get("value").asText(); + + assertFalse(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_2))); + + } + + /** + * ObserveComposite {"ids":["/5/0/7", "/5/0/5", "/5/0/3", "/3/0/9", "/19/1/0/0"]} - Ok + * ObserveCompositeCancel {"ids":["/5/0/7", "/5/0/5", "/5/0/3", "/3/0/9", "/19/1/0/0"]} - Ok + * @throws Exception + */ + @Test + public void testObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_This_Result_Content_Count_5() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + // ObserveComposite + String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; + String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; + String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; + String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; + String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; + String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + // ObserveCompositeCancel + actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + assertEquals("5", rpcActualResult.get("value").asText()); + + assertEquals(0, (Object) getCntObserveAll(deviceId)); + } + + /** + * ObserveComposite {"ids":["/3", "/5/0/3", "/19/1/0/0"]} - Ok + * ObserveCompositeCancel {"ids":["/3", "/5/0/3", "/19/1/0/0"]} - Ok + * @throws Exception + */ + @Test + public void testObserveCompositeOneObjectAnyResources_Result_CONTENT_CancelObserveComposite_This_Result_Content_Count_3() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + // ObserveComposite + String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; + String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; + String expectedIds = "[\"" + idVer_3_0_9 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; + String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + // ObserveCompositeCancel + actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + assertEquals("3", rpcActualResult.get("value").asText()); + + assertEquals(0, (Object) getCntObserveAll(deviceId)); + } + + /** + * ObserveComposite {"ids":["/3/0/9", "/3/0/14", "/5/0/3", "/19/1/0/0"]} - Ok + * ObserveCompositeCancel {"ids":["/3", "/19/1/0/0"]} - Ok + * last Observation + * @throws Exception + */ + @Test + public void testObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_OneObjectAnyResource_Result_Content_Count_4() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + // ObserveComposite + String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; + String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; + String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; + String expectedIdVer3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; + String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; + String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; + String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + awaitObserveReadAll(5, deviceId); + + // ObserveCompositeCancel + expectedIds = "[\"" + objectInstanceIdVer_5 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; + actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + assertEquals("4", rpcActualResult.get("value").asText()); + + String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); + ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); + String actualValues = rpcActualResultReadAll.get("value").asText(); + assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer3_0_9) + "\"]", actualValues); + } + + /** + * ObserveComposite {"ids":["/3/0/9", "/3/0/14", "/5/0/3", "/3/0/15", "/19/1/0/0"]} - Ok + * ObserveCompositeCancel {"ids":["/3/0/9", "/19/1/0/0", "/3]} - Ok + * last Observation + * @throws Exception + */ + @Test + public void testObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_OneResource_OneObjectAnyResource_Result_Content_Count_4() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + // ObserveComposite + sendCancelObserveAllWithAwait(deviceId); + String expectedIdVer3_0_14 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14; + String expectedIdVer3_0_15= objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_15; + String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; + String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; + String expectedIds = "[\"" + idVer_3_0_9 + "\", \"" + expectedIdVer3_0_14 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer3_0_15 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; + String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + // ObserveCompositeCancel + expectedIds = "[\"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\", \"" + objectIdVer_3 + "\"]"; + actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + assertEquals("4", rpcActualResult.get("value").asText()); + + String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); + ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); + String actualValues = rpcActualResultReadAll.get("value").asText(); + assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer5_0_3) + "\"]", actualValues); + } + + + private String sendObserve(String method, String params) throws Exception { + String sendRpcRequest; + if (params == null) { + sendRpcRequest = "{\"method\": \"" + method + "\"}"; + } + else { + sendRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"id\": \"" + params + "\"}}"; + } + return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, sendRpcRequest, String.class, status().isOk()); + } + + private String sendCompositeRPCByIds(String method, String paths) throws Exception { + String setRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"ids\":" + paths + "}}"; + return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); + } + + private String sendCompositeRPCByKeys(String method, String keys) throws Exception { + String setRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"keys\":" + keys + "}}"; + return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java index 6da48b0a79..039af357cc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java @@ -35,6 +35,7 @@ import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertObjectIdToVerId; public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegrationTest { @@ -132,8 +133,10 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration String expectedObjectInstanceId = pathIdVerToObjectId(expectedInstance); LwM2mPath expectedPath = new LwM2mPath(expectedObjectInstanceId); int expectedResource = lwM2MTestClient.getLeshanClient().getObjectTree().getObjectEnablers().get(expectedPath.getObjectId()).getObjectModel().resources.entrySet().stream().findAny().get().getKey(); + String ver = lwM2MTestClient.getLeshanClient().getObjectTree().getObjectEnablers().get(expectedPath.getObjectId()).getObjectModel().version; String expected = expectedInstance + "/" + expectedResource; - String actualResult = sendDiscover(expected); + String expectedVerId = convertObjectIdToVerId(expected, ver); + String actualResult = sendDiscover(expectedVerId); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String expectedResourceId = "<" + expectedObjectInstanceId + "/" + expectedResource + ">"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index d9679c92c4..c6d96f2756 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -17,84 +17,141 @@ package org.thingsboard.server.transport.lwm2m.rpc.sql; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.LwM2m.Version; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.server.registration.Registration; import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest; +import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; + +import java.util.Optional; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; @Slf4j -public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest { +public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationObserveTest { + + @SpyBean + DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest; + + @Test + public void testObserveReadAll_Count_2_CancelAll_Count_0_Ok() throws Exception { + String actualResultReadAll = sendRpcObserve("ObserveReadAll", null); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actualValuesReadAll = rpcActualResult.get("value").asText(); + assertEquals(2, actualValuesReadAll.split(",").length); + String expected = "\"SingleObservation:/19/0/0\""; + assertTrue(actualValuesReadAll.contains(expected)); + expected = "\"SingleObservation:/3/0/9\""; + assertTrue(actualValuesReadAll.contains(expected)); + } /** - * ObserveReadAll&ObserveCancelAll + * Observe "3_1.2/0/9" * @throws Exception */ @Test - public void testObserveReadAllNothingObservation_Result_CONTENT_Value_Count_0() throws Exception { - awaitObserveReadAll(2, deviceId); - String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; - String actualResult = sendRpcObserve("Observe", idVer_3_0_0); + public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + String idVer_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; + String actualResult = sendRpcObserve("Observe", idVer_3_0_9); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertTrue(rpcActualResult.get("value").asText().contains("LwM2mSingleResource")); - assertEquals(3, getCntObserveAll(deviceId)); - actualResult = sendRpcObserve("ObserveCancelAll", null); - rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - int cntObserveCancelAll = Integer.parseInt(rpcActualResult.get("value").asText()); - assertTrue(cntObserveCancelAll > 0); - assertEquals(0, getCntObserveAll(deviceId)); + assertEquals(Optional.of(1).get(), Optional.ofNullable(getCntObserveAll(deviceId)).get()); + + int cntUpdate = 3; + verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate)) + .onUpdateValueAfterReadResponse(Mockito.any(Registration.class), eq(idVer_3_0_9), Mockito.any(ReadResponse.class)); } /** - * Observe {"id":"/3/0/0"} + * Observe "3_1.2/0" * @throws Exception */ @Test - public void testObserveSingleResourceWithout_IdVer_1_0_Result_CONTENT_Value_SingleResource() throws Exception { - String expectedId = objectInstanceIdVer_9 + "/" + RESOURCE_ID_0; - String actualResult = sendRpcObserve("Observe", fromVersionedIdToObjectId(expectedId)); + public void testObserveOneObjectInstance_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + String idVer_3_0 = objectInstanceIdVer_3; + String actualResult = sendRpcObserve("Observe", idVer_3_0); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertTrue(rpcActualResult.get("value").asText().contains("LwM2mSingleResource")); + assertEquals(Optional.of(1).get(), Optional.ofNullable(getCntObserveAll(deviceId)).get()); + int cntUpdate = 3; + verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate)) + .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9)); } + /** - * Observe {"id":"/3_1.0/0/14"} + * Observe "3_1.2" * @throws Exception */ @Test - public void testObserveSingleResourceWith_IdVer_1_0_Result_CONTENT_Value_SingleResource() throws Exception { - String expectedId = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; - String actualResult = sendRpcObserve("Observe", expectedId); + public void testObserveOneObject_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + String idVer_3_0 = objectInstanceIdVer_3; + String actualResult = sendRpcObserve("Observe", idVer_3_0); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertTrue(rpcActualResult.get("value").asText().contains("LwM2mSingleResource")); + assertEquals(Optional.of(1).get(), Optional.ofNullable(getCntObserveAll(deviceId)).get()); + int cntUpdate = 3; + verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate)) + .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9)); } + /** - * Observe {"id":"/3_1.1/0/13"} + * Repeated request on Observe + * Observe {"id":"/3_1.2/0/0"} * @throws Exception */ @Test - public void testObserveWithBadVersion_Result_BadRequest_ErrorMsg_BadVersionMustBe1_0() throws Exception { + public void testObserveRepeated_Result_CONTENT_AddIfAbsent() throws Exception { + String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; + String actualResult = sendRpcObserve("Observe", idVer_3_0_0); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + actualResult = sendRpcObserve("Observe", idVer_3_0_0); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String expected = "LwM2mSingleResource [id=0"; + assertTrue(rpcActualResult.get("value").asText().contains(expected)); + } + + /** + * Observe {"id":"/3_1.?/0/13"} + * @throws Exception + */ + @Test + public void testObserveWithBadVersion_Result_BadRequest_ErrorMsg_BadVersionMustBe_Ver() throws Exception { String expectedInstance = (String) expectedInstances.stream().filter(path -> !((String)path).contains("_")).findFirst().get(); LwM2mPath expectedPath = new LwM2mPath(expectedInstance); int expectedResource = lwM2MTestClient.getLeshanClient().getObjectTree().getObjectEnablers().get(expectedPath.getObjectId()).getObjectModel().resources.entrySet().stream().findAny().get().getKey(); - String expectedId = "/" + expectedPath.getObjectId() + "_1.2" + "/" + expectedPath.getObjectInstanceId() + "/" + expectedResource; + String ver = lwM2MTestClient.getLeshanClient().getObjectTree().getObjectEnablers().get(expectedPath.getObjectId()).getObjectModel().version; + String expectedId = "/" + expectedPath.getObjectId() + "_" + Version.MAX + "/" + expectedPath.getObjectInstanceId() + "/" + expectedResource; String actualResult = sendRpcObserve("Observe", expectedId); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); - String expected = "Specified resource id " + expectedId +" is not valid version! Must be version: 1.0"; + String expected = "Specified resource id " + expectedId +" is not valid version! Must be version: " + ver; assertEquals(expected, rpcActualResult.get("error").asText()); } @@ -128,12 +185,11 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT } /** - * Repeated request on Observe - * Observe {"id":"/5/0/0"} + * Observe resource Write -> "/5/0/02" * @throws Exception */ @Test - public void testObserveRSourceNotRead_Result_METHOD_NOT_ALLOWED() throws Exception { + public void testObserveResourceNotRead_Result_METHOD_NOT_ALLOWED() throws Exception { String expectedId = objectInstanceIdVer_5 + "/" + RESOURCE_ID_0; sendRpcObserve("Observe", expectedId); String actualResult = sendRpcObserve("Observe", expectedId); @@ -141,54 +197,173 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText()); } + /** + * Observe resource Execute -> "/5/0/2" + * @throws Exception + */ + @Test + public void testObserveExecuteResource_Result_METHOD_NOT_ALLOWED() throws Exception { + String expectedId = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; + sendRpcObserve("Observe", expectedId); + String actual = sendRpcObserve("Observe", expectedId); + ObjectNode rpcActual = JacksonUtil.fromString(actual, ObjectNode.class); + assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActual.get("result").asText()); + } + /** * Repeated request on Observe - * Observe {"id":"/3_1.2/0/9"} + * Observe {"id":"/3_1.2/0/0"} * @throws Exception */ @Test - public void testObserveRepeatedRequestObserveOnDevice_Result_BAD_REQUEST_ErrorMsg_AlreadyRegistered() throws Exception { + public void testObserveRepeatedRequestObserveOnDevice_Result_CONTENT_PutIfAbsent() throws Exception { String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; String actualResult = sendRpcObserve("Observe", idVer_3_0_0); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); actualResult = sendRpcObserve("Observe", idVer_3_0_0); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); - String expected = "Observation is already registered!"; - assertEquals(expected, rpcActualResult.get("error").asText()); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String expected = "LwM2mSingleResource [id=0"; + assertTrue(rpcActualResult.get("value").asText().contains(expected)); } /** - * ObserveReadAll + * Observe {"id":["3"]} - Ok + * PreviousObservation contains "3/0/0" * @throws Exception */ @Test - public void testObserveReadAll_Result_CONTENT_Value_Contains_Paths_Count_ObserveReadAll() throws Exception { - String expectedId = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; - sendRpcObserve("Observe", expectedId); + public void testObserve_Result_CONTENT_ONE_PATH_PreviousObservation_CONTAINCE_OTHER_CurrentObservation() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + // "3/0/9" + String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; + String actualResult3_0_0 = sendRpcObserve("Observe", idVer_3_0_0); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult3_0_0, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + // "3" + String actualResult3 = sendRpcObserve("Observe", objectIdVer_3); + rpcActualResult = JacksonUtil.fromString(actualResult3, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + // PreviousObservation "3/0/0" change to CurrentObservation "3" String actualResultReadAll = sendRpcObserve("ObserveReadAll", null); - ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); - String actualValuesReadAll = rpcActualResultReadAll.get("value").asText(); - log.warn("ObserveReadAll: [{}]", actualValuesReadAll); - assertEquals(3, actualValuesReadAll.split(",").length); + rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actualValuesReadAll = rpcActualResult.get("value").asText(); + assertEquals(1, actualValuesReadAll.split(",").length); + String expected = "\"SingleObservation:/3\""; + assertTrue(actualValuesReadAll.contains(expected)); + } + + /** + * Observe {"id":["3/0/9"]} - Ok + * PreviousObservation contains "3" + * @throws Exception + */ + @Test + public void testObserve_Result_CONTENT_ONE_PATH_CurrentObservation_CONTAINCE_OTHER_PreviousObservation() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + // "3" + String actualResult3 = sendRpcObserve("Observe", objectIdVer_3); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult3, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + // "3/0/0" + String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; + String actualResult3_0_0 = sendRpcObserve("Observe", idVer_3_0_0); + rpcActualResult = JacksonUtil.fromString(actualResult3_0_0, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + // PreviousObservation "3" contains CurrentObservation "3/0/0" + String actualResultReadAll = sendRpcObserve("ObserveReadAll", null); + rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actualValuesReadAll = rpcActualResult.get("value").asText(); + assertEquals(1, actualValuesReadAll.split(",").length); + String expected = "\"SingleObservation:/3\""; + assertTrue(actualValuesReadAll.contains(expected)); } /** - * ObserveCancel {"id":"/3/0/3"} - * ObserveCancel {"id":"/5/0/3"} + * Observe {"id":"/3/0/9"} + * ObserveCancel {"id":"/3/0/9"} */ @Test - public void testObserveCancelOneResource_Result_CONTENT_Value_Count_1() throws Exception { - sendRpcObserve("ObserveCancelAll", null); - String expectedId_3_0_3 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_3; - String expectedId_5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; - sendRpcObserve("Observe", expectedId_3_0_3); - sendRpcObserve("Observe", expectedId_5_0_3); - String actualResult = sendRpcObserve("ObserveCancel", expectedId_3_0_3); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + public void testObserveResource_ObserveCancelResource_Result_CONTENT_Count_1() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + + String expectedId_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; + sendRpcObserve("Observe", expectedId_3_0_9); + String actualResultReadAll = sendRpcObserve("ObserveReadAll", null); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actualValuesReadAll = rpcActualResult.get("value").asText(); + assertEquals(1, actualValuesReadAll.split(",").length); + String expected = "\"SingleObservation:" + fromVersionedIdToObjectId(expectedId_3_0_9) + "\""; + assertTrue(actualValuesReadAll.contains(expected)); + + // cancel observe "/3_1.2/0/9" + String actualResult = sendRpcObserve("ObserveCancel", expectedId_3_0_9); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + assertEquals("1", rpcActualResult.get("value").asText()); + } + + + /** + * Observe {"id":"/3"} + * ObserveCancel {"id":"/3/0/9"} + */ + @Test + public void testObserveObject_ObserveCancelOneResource_Result_INTERNAL_SERVER_ERROR_Than_Cancel_ObserveObject_Result_CONTENT_Count_1() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + + String expectedId_3 = objectIdVer_3; + sendRpcObserve("Observe", expectedId_3); + String actualResultReadAll = sendRpcObserve("ObserveReadAll", null); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actualValuesReadAll = rpcActualResult.get("value").asText(); + assertEquals(1, actualValuesReadAll.split(",").length); + String expected = "\"SingleObservation:" + fromVersionedIdToObjectId(expectedId_3) + "\""; + assertTrue(actualValuesReadAll.contains(expected)); + + // cancel observe "/3_1.2/0/9" + String expectedId_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; + String actualResult = sendRpcObserve("ObserveCancel", expectedId_3_0_9); + String expectedValue = "for observation path " + fromVersionedIdToObjectId(expectedId_3_0_9) + ", that includes this observation path " + fromVersionedIdToObjectId(objectIdVer_3); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText()); + assertTrue(rpcActualResult.get("error").asText().contains(expectedValue)); + + // cancel observe "/3_1.2" + actualResult = sendRpcObserve("ObserveCancel", expectedId_3); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + assertEquals("1", rpcActualResult.get("value").asText()); + } + + /** + * Observe {"id":"/3/0/9"} + * ObserveCancel {"id":"/3"} + */ + @Test + public void testObserveResource_ObserveCancelObject_Result_CONTENT_Count_1() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + + String expectedId_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; + sendRpcObserve("Observe", expectedId_3_0_9); + String actualResultReadAll = sendRpcObserve("ObserveReadAll", null); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String actualValuesReadAll = rpcActualResult.get("value").asText(); + assertEquals(1, actualValuesReadAll.split(",").length); + String expected = "\"SingleObservation:" + fromVersionedIdToObjectId(expectedId_3_0_9) + "\""; + assertTrue(actualValuesReadAll.contains(expected)); + + // cancel observe "/3_1.2" + String expectedId_3 = objectIdVer_3; + String actualResult = sendRpcObserve("ObserveCancel", expectedId_3); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals("1", rpcActualResult.get("value").asText()); } diff --git a/application/src/test/resources/lwm2m/5.xml b/application/src/test/resources/lwm2m/5.xml index 5133f7d9ab..ddcea323b0 100644 --- a/application/src/test/resources/lwm2m/5.xml +++ b/application/src/test/resources/lwm2m/5.xml @@ -70,7 +70,7 @@ A LwM2M Server MUST support block-wise transfer. Other protocols, such as HTTP/H 5 urn:oma:lwm2m:oma:5 1.0 - 1.0 + 1.2 Single Optional diff --git a/application/src/test/resources/lwm2m/9.xml b/application/src/test/resources/lwm2m/9.xml index 4186b1248d..7c78620d85 100644 --- a/application/src/test/resources/lwm2m/9.xml +++ b/application/src/test/resources/lwm2m/9.xml @@ -64,7 +64,7 @@ LEGAL DISCLAIMER 9 urn:oma:lwm2m:oma:9 1.0 - 1.0 + 1.1 Multiple Optional diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 5c37bbcf89..ca4a9ab2c7 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -97,7 +97,7 @@ org.awaitility awaitility - test + 4.2.0 org.eclipse.californium diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index 1f1382ecf5..0d7b25616e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -131,6 +131,7 @@ public class LwM2MTransportBootstrapService { // Set Californium Configuration endpointsBuilder.setConfiguration(serverCoapConfig); + serverConfig.setCoapConfig(serverCoapConfig); // Create CoAP endpoint diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java index 018648100d..845a1e725b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java @@ -50,7 +50,7 @@ import java.util.stream.Collectors; import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; import static org.eclipse.leshan.server.bootstrap.BootstrapUtil.toWriteRequest; -import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_DEFAULT_SHORT_ID; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_DEFAULT_SHORT_ID_0; @Slf4j public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTaskProvider { @@ -106,7 +106,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask "Bootstrap Discover return error {} : to continue bootstrap session without autoIdForSecurityObject mode. {}", discoverResponse, session); } - if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID) == null) { + if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID_0) == null) { log.error( "Unable to find bootstrap server instance in Security Object (0) in response {}: unable to continue bootstrap session with autoIdForSecurityObject mode. {}", discoverResponse, session); @@ -170,7 +170,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId()); } else { if (!this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(0)) { - this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(BOOTSTRAP_DEFAULT_SHORT_ID, path.getObjectInstanceId()); + this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(BOOTSTRAP_DEFAULT_SHORT_ID_0, path.getObjectInstanceId()); } else { log.error("Invalid bootstrapSecurityInstance by [{}]", path.getObjectInstanceId()); } @@ -244,7 +244,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask Integer bootstrapServerIdNew = null; // handle security int lwm2mSecurityInstanceId = 0; - int bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID); + int bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID_0); for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfig.security).values()) { if (security.bootstrapServer) { requestsWrite.add(toWriteRequest(bootstrapSecurityInstanceId, security, contentFormat)); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java index 2b7ce44e10..f73a419e3e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.lwm2m.config; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.elements.config.Configuration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -105,6 +106,10 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { @Setter private List networkConfig; + @Getter + @Setter + private Configuration coapConfig; + @Bean @ConfigurationProperties(prefix = "transport.lwm2m.server.security.credentials") public SslCredentialsConfig lwm2mServerCredentials() { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java index a29a37c766..392f3158fe 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -93,9 +93,10 @@ public class LwM2mServerListener { @Override public void cancelled(Observation observation) { - //TODO: should be able to use CompositeObservation - log.trace("Canceled Observation {}.", ((SingleObservation)observation).getPath()); - } + log.info("Canceled Observation [RegistrationId:{}: {}].", observation.getRegistrationId(), observation instanceof SingleObservation ? + "SingleObservation: " + ((SingleObservation) observation).getPath() : + "CompositeObservation: " + ((CompositeObservation) observation).getPaths()); + } @Override public void onResponse(SingleObservation observation, Registration registration, ObserveResponse response) { @@ -106,21 +107,25 @@ public class LwM2mServerListener { @Override public void onResponse(CompositeObservation observation, Registration registration, ObserveCompositeResponse response) { - throw new RuntimeException("Not implemented yet!"); + log.trace("Update Composite Observation [{}: {}].", observation.getRegistrationId(), observation.getPaths()); + service.onUpdateValueAfterReadCompositeResponse(registration, response); } @Override public void onError(Observation observation, Registration registration, Exception error) { if (error != null) { - //TODO: should be able to use CompositeObservation - log.debug("Unable to handle notification of [{}:{}] [{}]", observation.getRegistrationId(), ((SingleObservation)observation).getPath(), error.getMessage()); + var path = observation instanceof SingleObservation ? "Single Observation Cancel: " + ((SingleObservation) observation).getPath() : "Composite Observation Cancel: " + ((CompositeObservation) observation).getPaths(); + var msgError = path + ": " + error.getMessage(); + log.trace("Unable to handle notification [RegistrationId:{}]: [{}].", observation.getRegistrationId(), msgError); + service.onErrorObservation(registration, msgError); } } @Override public void newObservation(Observation observation, Registration registration) { - //TODO: should be able to use CompositeObservation - log.trace("Successful start newObservation {}.", ((SingleObservation)observation).getPath()); + log.trace("Successful start newObservation [RegistrationId:{}: {}].", observation.getRegistrationId(), observation instanceof SingleObservation ? + "Single: " + ((SingleObservation) observation).getPath() : + "Composite: " + ((CompositeObservation) observation).getPaths()); } }; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java index 7b9eff0cc5..9c8b37b206 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java @@ -55,7 +55,7 @@ public class LwM2mSessionMsgListener implements GenericFutureListener observations = context.getServer().getObservationService().getObservations(client.getRegistration()); - //TODO: should be able to use CompositeObservation - if (observations.stream().noneMatch(observation -> ((SingleObservation)observation).getPath().equals(resultIds))) { - ObserveRequest downlink; - ContentFormat contentFormat = getReadRequestContentFormat(client, request, modelProvider); - if (resultIds.isResource()) { - downlink = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId()); - } else if (resultIds.isObjectInstance()) { - downlink = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId()); - } else { - downlink = new ObserveRequest(contentFormat, resultIds.getObjectId()); - } - log.info("[{}] Send observation: {}.", client.getEndpoint(), request.getVersionedId()); - sendSimpleRequest(client, downlink, request.getTimeout(), callback); + ObserveRequest downlink; + ContentFormat contentFormat = getReadRequestContentFormat(client, request, modelProvider); + if (resultIds.isResourceInstance()) { + downlink = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), resultIds.getResourceInstanceId()); + } else if (resultIds.isResource()) { + downlink = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId()); + } else if (resultIds.isObjectInstance()) { + downlink = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId()); } else { - callback.onValidationError(resultIds.toString(), "Observation is already registered!"); + downlink = new ObserveRequest(contentFormat, resultIds.getObjectId()); } + log.info("[{}] Send observation: {}.", client.getEndpoint(), request.getVersionedId()); + sendSimpleRequest(client, downlink, request.getTimeout(), callback); } catch (InvalidRequestException e) { callback.onValidationError(request.toString(), e.getMessage()); } @@ -197,11 +202,72 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im @Override public void sendObserveAllRequest(LwM2mClient client, TbLwM2MObserveAllRequest request, DownlinkRequestCallback> callback) { Set observations = context.getServer().getObservationService().getObservations(client.getRegistration()); - //TODO: should be able to use CompositeObservation - Set paths = observations.stream().map(observation -> ((SingleObservation)observation).getPath().toString()).collect(Collectors.toUnmodifiableSet()); + Set paths = new LinkedHashSet<>(); + observations.stream().forEach(observation -> { + if (observation instanceof SingleObservation) { + paths.add("SingleObservation:" + ((SingleObservation) observation).getPath().toString()); + } else { + List listPath = ((CompositeObservation) observation).getPaths(); + List pathsComposite = listPath.stream().map(lwM2mPath -> (lwM2mPath.toString())).collect(Collectors.toList()); + Set pathsCompositeSort = new TreeSet<>(); + if (pathsComposite.size() == 1) { + pathsCompositeSort.add("CompositeObservation: [" + pathsComposite.get(0) + "]"); + } else if (pathsComposite.size() > 1) { + List sort = new LinkedList<>(); + sort.addAll(pathsComposite); + sort.set(0, "CompositeObservation: [" + sort.get(0)); + sort.set(pathsComposite.size()-1, sort.get(pathsComposite.size()-1) + "]"); + pathsCompositeSort = new LinkedHashSet<>(sort); + } + paths.addAll(pathsCompositeSort); + } + + }); callback.onSuccess(request, paths); } + /** + * if resource (SingleObservation or in CompositeObservation) is already registered - return BAD REQUEST + */ + @Override + public void sendObserveCompositeRequest(LwM2mClient client, TbLwM2MObserveCompositeRequest request, DownlinkRequestCallback callback) { + try { + log.trace("[{}] Send Composite observation: [{}].", client.getEndpoint(), request.getObjectIds()); + ContentFormat compositeContentFormat = this.findFirstContentFormatForComposite(client.getClientSupportContentFormats()); + ObserveCompositeRequest downlink = new ObserveCompositeRequest(compositeContentFormat, compositeContentFormat, request.getObjectIds()); + sendCompositeRequest(client, downlink, this.config.getTimeout(), callback); + } catch (InvalidRequestException e) { + callback.onValidationError(request.toString(), e.getMessage()); + } + } + + @Override + public void sendCancelObserveCompositeRequest(LwM2mClient client, TbLwM2MCancelObserveCompositeRequest request, DownlinkRequestCallback callback) { + Set observations = context.getServer().getObservationService().getObservations(client.getRegistration()); + List listPath = LwM2mPath.getLwM2mPathList(Arrays.asList(request.getObjectIds())); + Optional observationOpt = Optional.ofNullable(observations.stream().filter(observation -> observation instanceof CompositeObservation && ((CompositeObservation) observation).getPaths().equals(listPath)).findFirst().orElse(null)); + int cnt = 0; + if (observationOpt.isPresent()) { + cnt = context.getServer().getObservationService().cancelCompositeObservations(client.getRegistration(), request.getObjectIds()); + callback.onSuccess(request, cnt); + } else { + Set lwPaths = new HashSet<>(); + observations.forEach(obs -> { + LwM2mPath lwPathObs = ((SingleObservation) obs).getPath(); + listPath.forEach(nodePath -> { + if(nodePath.equals(lwPathObs) || lwPathObs.startWith(nodePath)){ // nodePath = "3", lwPathObs = "3/0/9": cancel for tne all lwPathObs + lwPaths.add(nodePath.toString()); + } + }); + }); + for (String nodePath : lwPaths) { + cnt += context.getServer().getObservationService().cancelObservations(client.getRegistration(), nodePath); + } + } + callback.onSuccess(request, cnt); + } + @Override public void sendDiscoverAllRequest(LwM2mClient client, TbLwM2MDiscoverAllRequest request, DownlinkRequestCallback> callback) { callback.onSuccess(request, Arrays.stream(client.getRegistration().getSortedObjectLinks()).map(Link::toCoreLinkFormat).collect(Collectors.toList())); @@ -258,7 +324,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } @Override - public void sendCancelAllRequest(LwM2mClient client, TbLwM2MCancelAllRequest request, DownlinkRequestCallback callback) { + public void sendCancelObserveAllRequest(LwM2mClient client, TbLwM2MCancelAllRequest request, DownlinkRequestCallback callback) { int observeCancelCnt = context.getServer().getObservationService().cancelObservations(client.getRegistration()); callback.onSuccess(request, observeCancelCnt); } @@ -709,4 +775,13 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im }); return compositeContentFormat.get() != null ? compositeContentFormat.get() : defaultValue; } + + private static boolean containsObserveComposite(List l1, List l2) { + for (T elem : l1) { + if (l2.contains(elem)) { + return true; + } + } + return false; + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java index a9d49e6dae..2987de1fc2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/LwM2mDownlinkMsgHandler.java @@ -19,6 +19,7 @@ import org.eclipse.leshan.core.request.CreateRequest; import org.eclipse.leshan.core.request.DeleteRequest; import org.eclipse.leshan.core.request.DiscoverRequest; import org.eclipse.leshan.core.request.ExecuteRequest; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.core.request.ReadCompositeRequest; import org.eclipse.leshan.core.request.ReadRequest; @@ -29,6 +30,7 @@ import org.eclipse.leshan.core.response.CreateResponse; import org.eclipse.leshan.core.response.DeleteResponse; import org.eclipse.leshan.core.response.DiscoverResponse; import org.eclipse.leshan.core.response.ExecuteResponse; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.core.response.ReadCompositeResponse; import org.eclipse.leshan.core.response.ReadResponse; @@ -36,6 +38,8 @@ import org.eclipse.leshan.core.response.WriteAttributesResponse; import org.eclipse.leshan.core.response.WriteCompositeResponse; import org.eclipse.leshan.core.response.WriteResponse; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MCancelObserveCompositeRequest; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeRequest; import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeRequest; import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcWriteCompositeRequest; @@ -58,7 +62,11 @@ public interface LwM2mDownlinkMsgHandler { void sendCancelObserveRequest(LwM2mClient client, TbLwM2MCancelObserveRequest request, DownlinkRequestCallback callback); - void sendCancelAllRequest(LwM2mClient client, TbLwM2MCancelAllRequest request, DownlinkRequestCallback callback); + void sendCancelObserveAllRequest(LwM2mClient client, TbLwM2MCancelAllRequest request, DownlinkRequestCallback callback); + + void sendObserveCompositeRequest(LwM2mClient client, TbLwM2MObserveCompositeRequest request, DownlinkRequestCallback callback); + + void sendCancelObserveCompositeRequest(LwM2mClient client, TbLwM2MCancelObserveCompositeRequest request, DownlinkRequestCallback callback); void sendDiscoverRequest(LwM2mClient client, TbLwM2MDiscoverRequest request, DownlinkRequestCallback callback); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java new file mode 100644 index 0000000000..9eb3797ea4 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java @@ -0,0 +1,40 @@ +/** + * 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.lwm2m.server.downlink.composite; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.downlink.AbstractTbLwM2MRequestCallback; +import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; + +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LOG_LWM2M_INFO; + +@Slf4j +public class TbLwM2MCancelObserveCompositeCallback extends AbstractTbLwM2MRequestCallback { + + private final String [] versionedIds; + + public TbLwM2MCancelObserveCompositeCallback(LwM2MTelemetryLogService logService, LwM2mClient client, String [] versionedIds) { + super(logService, client); + this.versionedIds = versionedIds; + } + + @Override + public void onSuccess(TbLwM2MCancelObserveCompositeRequest request, Integer canceledSubscriptionsCount) { + log.trace("[{}] Cancel composite observation of [{}] successful: {}", client.getEndpoint(), this.versionedIds, canceledSubscriptionsCount); + logService.log(client, String.format("[%s]: Cancel Composite Observe for [%s] successful. Result: [%s]", LOG_LWM2M_INFO, this.versionedIds, canceledSubscriptionsCount)); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java new file mode 100644 index 0000000000..c3d4608bf5 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java @@ -0,0 +1,32 @@ +/** + * 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.lwm2m.server.downlink.composite; + +import lombok.Builder; +import org.thingsboard.server.transport.lwm2m.server.LwM2MOperationType; + +public class TbLwM2MCancelObserveCompositeRequest extends AbstractTbLwM2MTargetedDownlinkCompositeRequest { + + @Builder + private TbLwM2MCancelObserveCompositeRequest(String [] versionedIds, long timeout) { + super(versionedIds, timeout); + } + + @Override + public LwM2MOperationType getType() { + return LwM2MOperationType.OBSERVE_COMPOSITE_CANCEL; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java new file mode 100644 index 0000000000..6b3855bb3c --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java @@ -0,0 +1,39 @@ +/** + * 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.lwm2m.server.downlink.composite; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MUplinkTargetedCallback; +import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; + +@Slf4j +public class TbLwM2MObserveCompositeCallback extends TbLwM2MUplinkTargetedCallback { + + public TbLwM2MObserveCompositeCallback(LwM2mUplinkMsgHandler handler, LwM2MTelemetryLogService logService, LwM2mClient client, String[] versionedIds) { + super(handler, logService, client, versionedIds); + } + + @Override + public void onSuccess(ObserveCompositeRequest request, ObserveCompositeResponse response) { + super.onSuccess(request, response); + handler.onUpdateValueAfterReadCompositeResponse(client.getRegistration(), response); + } + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java new file mode 100644 index 0000000000..93f9ddf726 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java @@ -0,0 +1,51 @@ +/** + * 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.lwm2m.server.downlink.composite; + +import lombok.Builder; +import lombok.Getter; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; +import org.thingsboard.server.transport.lwm2m.server.LwM2MOperationType; +import org.thingsboard.server.transport.lwm2m.server.downlink.HasContentFormat; + +import java.util.Optional; + +public class TbLwM2MObserveCompositeRequest extends AbstractTbLwM2MTargetedDownlinkCompositeRequest implements HasContentFormat { + + + private final Optional requestContentFormatOpt; + + @Getter + private final ContentFormat responseContentFormat; + + @Builder + private TbLwM2MObserveCompositeRequest(String [] versionedIds, long timeout, ContentFormat requestContentFormat, ContentFormat responseContentFormat) { + super(versionedIds, timeout); + this.requestContentFormatOpt = Optional.ofNullable(requestContentFormat); + this.responseContentFormat = responseContentFormat; + } + + @Override + public LwM2MOperationType getType() { + return LwM2MOperationType.OBSERVE_COMPOSITE; + } + + @Override + public Optional getRequestContentFormat() { + return this.requestContentFormatOpt; + } +} 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 4e43d7b653..734522134f 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 @@ -233,19 +233,19 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl @Override public void onCurrentFirmwareNameUpdate(LwM2mClient client, String name) { - log.debug("[{}] Current fw name: {}", client.getEndpoint(), name); + log.trace("[{}] Current fw name: {}", client.getEndpoint(), name); getOrInitFwInfo(client).setCurrentName(name); } @Override public void onCurrentSoftwareNameUpdate(LwM2mClient client, String name) { - log.debug("[{}] Current sw name: {}", client.getEndpoint(), name); + log.trace("[{}] Current sw name: {}", client.getEndpoint(), name); getOrInitSwInfo(client).setCurrentName(name); } @Override public void onFirmwareStrategyUpdate(LwM2mClient client, OtherConfiguration configuration) { - log.debug("[{}] Current fw strategy: {}", client.getEndpoint(), configuration.getFwUpdateStrategy()); + log.trace("[{}] Current fw strategy: {}", client.getEndpoint(), configuration.getFwUpdateStrategy()); startFirmwareUpdateIfNeeded(client, initFwStrategy(client, configuration)); } @@ -258,7 +258,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl @Override public void onCurrentSoftwareStrategyUpdate(LwM2mClient client, OtherConfiguration configuration) { - log.debug("[{}] Current sw strategy: {}", client.getEndpoint(), configuration.getSwUpdateStrategy()); + log.trace("[{}] Current sw strategy: {}", client.getEndpoint(), configuration.getSwUpdateStrategy()); startSoftwareUpdateIfNeeded(client, initSwStrategy(client, configuration)); } @@ -271,21 +271,21 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl @Override public void onCurrentFirmwareVersion3Update(LwM2mClient client, String version) { - log.debug("[{}] Current fw version(3): {}", client.getEndpoint(), version); + log.trace("[{}] Current fw version(3): {}", client.getEndpoint(), version); LwM2MClientFwOtaInfo fwInfo = getOrInitFwInfo(client); fwInfo.setCurrentVersion3(version); } @Override public void onCurrentFirmwareVersionUpdate(LwM2mClient client, String version) { - log.debug("[{}] Current fw version(5): {}", client.getEndpoint(), version); + log.trace("[{}] Current fw version(5): {}", client.getEndpoint(), version); LwM2MClientFwOtaInfo fwInfo = getOrInitFwInfo(client); fwInfo.setCurrentVersion(version); } @Override public void onCurrentFirmwareStateUpdate(LwM2mClient client, Long stateCode) { - log.debug("[{}] Current fw state: {}", client.getEndpoint(), stateCode); + log.trace("[{}] Current fw state: {}", client.getEndpoint(), stateCode); LwM2MClientFwOtaInfo fwInfo = getOrInitFwInfo(client); FirmwareUpdateState state = FirmwareUpdateState.fromStateFwByCode(stateCode.intValue()); if (FirmwareUpdateState.DOWNLOADED.equals(state)) { @@ -309,7 +309,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl @Override public void onCurrentFirmwareResultUpdate(LwM2mClient client, Long code) { - log.debug("[{}] Current fw result: {}", client.getEndpoint(), code); + log.trace("[{}] Current fw result: {}", client.getEndpoint(), code); LwM2MClientFwOtaInfo fwInfo = getOrInitFwInfo(client); FirmwareUpdateResult result = FirmwareUpdateResult.fromUpdateResultFwByCode(code.intValue()); Optional status = toOtaPackageUpdateStatus(result); @@ -338,26 +338,26 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl @Override public void onCurrentFirmwareDeliveryMethodUpdate(LwM2mClient client, Long value) { - log.debug("[{}] Current fw delivery method: {}", client.getEndpoint(), value); + log.trace("[{}] Current fw delivery method: {}", client.getEndpoint(), value); LwM2MClientFwOtaInfo fwInfo = getOrInitFwInfo(client); fwInfo.setDeliveryMethod(value.intValue()); } @Override public void onCurrentSoftwareVersion3Update(LwM2mClient client, String version) { - log.debug("[{}] Current sw version(3): {}", client.getEndpoint(), version); + log.trace("[{}] Current sw version(3): {}", client.getEndpoint(), version); getOrInitSwInfo(client).setCurrentVersion3(version); } @Override public void onCurrentSoftwareVersionUpdate(LwM2mClient client, String version) { - log.debug("[{}] Current sw version(9): {}", client.getEndpoint(), version); + log.trace("[{}] Current sw version(9): {}", client.getEndpoint(), version); getOrInitSwInfo(client).setCurrentVersion(version); } @Override public void onCurrentSoftwareStateUpdate(LwM2mClient client, Long stateCode) { - log.debug("[{}] Current sw state: {}", client.getEndpoint(), stateCode); + log.trace("[{}] Current sw state: {}", client.getEndpoint(), stateCode); LwM2MClientSwOtaInfo swInfo = getOrInitSwInfo(client); SoftwareUpdateState state = SoftwareUpdateState.fromUpdateStateSwByCode(stateCode.intValue()); if (SoftwareUpdateState.INITIAL.equals(state)) { @@ -375,7 +375,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl @Override public void onCurrentSoftwareResultUpdate(LwM2mClient client, Long code) { - log.debug("[{}] Current sw result: {}", client.getEndpoint(), code); + log.trace("[{}] Current sw result: {}", client.getEndpoint(), code); LwM2MClientSwOtaInfo swInfo = getOrInitSwInfo(client); SoftwareUpdateResult result = SoftwareUpdateResult.fromUpdateResultSwByCode(code.intValue()); Optional status = toOtaPackageUpdateStatus(result); @@ -417,14 +417,14 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl private void startFirmwareUpdateIfNeeded(LwM2mClient client, LwM2MClientFwOtaInfo fwInfo) { try { if (!fwInfo.isSupported() && fwInfo.isAssigned()) { - log.debug("[{}] Fw update is not supported: {}", client.getEndpoint(), fwInfo); + log.trace("[{}] Fw update is not supported: {}", client.getEndpoint(), fwInfo); sendStateUpdateToTelemetry(client, fwInfo, OtaPackageUpdateStatus.FAILED, "Client does not support firmware update or profile misconfiguration!"); } else if (fwInfo.isUpdateRequired()) { if (StringUtils.isNotEmpty(fwInfo.getTargetUrl())) { - log.debug("[{}] Starting update to [{}{}][] using URL: {}", client.getEndpoint(), fwInfo.getTargetName(), fwInfo.getTargetVersion(), fwInfo.getTargetUrl()); + log.trace("[{}] Starting update to [{}{}][] using URL: {}", client.getEndpoint(), fwInfo.getTargetName(), fwInfo.getTargetVersion(), fwInfo.getTargetUrl()); startUpdateUsingUrl(client, FW_URL_ID, fwInfo.getTargetUrl()); } else { - log.debug("[{}] Starting update to [{}{}] using binary", client.getEndpoint(), fwInfo.getTargetName(), fwInfo.getTargetVersion()); + log.trace("[{}] Starting update to [{}{}] using binary", client.getEndpoint(), fwInfo.getTargetName(), fwInfo.getTargetVersion()); startUpdateUsingBinary(client, fwInfo); } } else if (fwInfo.getResult() != null && fwInfo.getResult().getCode() > UPDATE_SUCCESSFULLY.getCode()) { @@ -440,18 +440,18 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl private void startSoftwareUpdateIfNeeded(LwM2mClient client, LwM2MClientSwOtaInfo swInfo) { try { if (!swInfo.isSupported() && swInfo.isAssigned()) { - log.debug("[{}] Sw update is not supported: {}", client.getEndpoint(), swInfo); + log.trace("[{}] Sw update is not supported: {}", client.getEndpoint(), swInfo); sendStateUpdateToTelemetry(client, swInfo, OtaPackageUpdateStatus.FAILED, "Client does not support software update or profile misconfiguration!"); } else if (swInfo.isUpdateRequired()) { if (SoftwareUpdateState.INSTALLED.equals(swInfo.getUpdateState())) { - log.debug("[{}] Attempt to restore the update state: {}", client.getEndpoint(), swInfo.getUpdateState()); + log.trace("[{}] Attempt to restore the update state: {}", client.getEndpoint(), swInfo.getUpdateState()); executeSwUninstallForUpdate(client); } else { if (StringUtils.isNotEmpty(swInfo.getTargetUrl())) { - log.debug("[{}] Starting update to [{}{}] using URL: {}", client.getEndpoint(), swInfo.getTargetName(), swInfo.getTargetVersion(), swInfo.getTargetUrl()); + log.trace("[{}] Starting update to [{}{}] using URL: {}", client.getEndpoint(), swInfo.getTargetName(), swInfo.getTargetVersion(), swInfo.getTargetUrl()); startUpdateUsingUrl(client, SW_PACKAGE_URI_ID, swInfo.getTargetUrl()); } else { - log.debug("[{}] Starting update to [{}{}] using binary", client.getEndpoint(), swInfo.getTargetName(), swInfo.getTargetVersion()); + log.trace("[{}] Starting update to [{}{}] using binary", client.getEndpoint(), swInfo.getTargetName(), swInfo.getTargetVersion()); startUpdateUsingBinary(client, swInfo); } } @@ -566,18 +566,21 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } private void executeFwUpdate(LwM2mClient client) { - TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(FW_EXECUTE_ID).timeout(clientContext.getRequestTimeout(client)).build(); - downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, FW_EXECUTE_ID)); + String fwExecuteVerId = convertObjectIdToVersionedId(FW_EXECUTE_ID, client.getRegistration()); + TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(fwExecuteVerId).timeout(clientContext.getRequestTimeout(client)).build(); + downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, fwExecuteVerId)); } private void executeSwInstall(LwM2mClient client) { - TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(SW_INSTALL_ID).timeout(clientContext.getRequestTimeout(client)).build(); - downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, SW_INSTALL_ID)); + String swInstallVerId = convertObjectIdToVersionedId(SW_INSTALL_ID, client.getRegistration()); + TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInstallVerId).timeout(clientContext.getRequestTimeout(client)).build(); + downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInstallVerId)); } private void executeSwUninstallForUpdate(LwM2mClient client) { - TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(SW_UN_INSTALL_ID).params("1").timeout(clientContext.getRequestTimeout(client)).build(); - downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, SW_INSTALL_ID)); + String swInInstallVerId = convertObjectIdToVersionedId(SW_UN_INSTALL_ID, client.getRegistration()); + TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInInstallVerId).params("1").timeout(clientContext.getRequestTimeout(client)).build(); + downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInInstallVerId)); } private Optional getAttributeValue(List attrs, String keyName) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java index c725d4f070..514ee8719b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java @@ -58,10 +58,16 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttrib import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteReplaceRequest; import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteResponseCallback; import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteUpdateRequest; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MCancelObserveCompositeCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MCancelObserveCompositeRequest; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeRequest; import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeCallback; import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeRequest; import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MWriteResponseCompositeCallback; import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; +import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcCancelObserveCompositeCallback; +import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcObserveResponseCompositeCallback; import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcReadCompositeRequest; import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcReadResponseCompositeCallback; import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcWriteCompositeRequest; @@ -162,6 +168,12 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { case WRITE_COMPOSITE: sendWriteCompositeRequest(client, rpcRequest); break; + case OBSERVE_COMPOSITE: + sendObserveCompositeRequest(client, rpcRequest); + break; + case OBSERVE_COMPOSITE_CANCEL: + sendCancelObserveCompositeRequest(client, rpcRequest); + break; default: throw new IllegalArgumentException("Unsupported operation: " + operationType.name()); } @@ -382,6 +394,25 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { downlinkHandler.sendCancelObserveRequest(client, downlink, rpcCallback); } + + private void sendObserveCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) { + String[] versionedIds = getIdsFromParameters(client, requestMsg); + TbLwM2MObserveCompositeRequest request = TbLwM2MObserveCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(client)).build(); + var mainCallback = new TbLwM2MObserveCompositeCallback(uplinkHandler, logService, client, versionedIds); + var rpcCallback = new RpcObserveResponseCompositeCallback(transportService, client, requestMsg, mainCallback); + downlinkHandler.sendObserveCompositeRequest(client, request, rpcCallback); + } + + private void sendCancelObserveCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) { + String[] versionedIds = getIdsFromParameters(client, requestMsg); + TbLwM2MCancelObserveCompositeRequest request = TbLwM2MCancelObserveCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(client)).build(); + var mainCallback = new TbLwM2MCancelObserveCompositeCallback(logService, client, versionedIds); + var rpcCallback = new RpcCancelObserveCompositeCallback(transportService, client, requestMsg, mainCallback); + downlinkHandler.sendCancelObserveCompositeRequest(client, request, rpcCallback); + } + + + private void sendDeleteRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) { TbLwM2MDeleteRequest downlink = TbLwM2MDeleteRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MDeleteCallback(logService, client, versionedId); @@ -393,7 +424,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { TbLwM2MCancelAllRequest downlink = TbLwM2MCancelAllRequest.builder().timeout(clientContext.getRequestTimeout(client)).build(); var mainCallback = new TbLwM2MCancelAllObserveCallback(logService, client); var rpcCallback = new RpcCancelAllObserveCallback(transportService, client, requestMsg, mainCallback); - downlinkHandler.sendCancelAllRequest(client, downlink, rpcCallback); + downlinkHandler.sendCancelObserveAllRequest(client, downlink, rpcCallback); } private String getIdFromParameters(LwM2mClient client, LwM2MRpcRequestHeader header) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.java new file mode 100644 index 0000000000..b011317ee3 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.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.lwm2m.server.rpc.composite; + +import org.eclipse.leshan.core.ResponseCode; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MCancelObserveCompositeRequest; +import org.thingsboard.server.transport.lwm2m.server.rpc.LwM2MRpcResponseBody; +import org.thingsboard.server.transport.lwm2m.server.rpc.RpcDownlinkRequestCallbackProxy; + +public class RpcCancelObserveCompositeCallback extends RpcDownlinkRequestCallbackProxy { + + public RpcCancelObserveCompositeCallback(TransportService transportService, LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, DownlinkRequestCallback callback) { + super(transportService, client, requestMsg, callback); + } + + @Override + protected void sendRpcReplyOnSuccess(Integer response) { + reply(LwM2MRpcResponseBody.builder().result(ResponseCode.CONTENT.getName()).value(response.toString()).build()); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java new file mode 100644 index 0000000000..6768a70bdf --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java @@ -0,0 +1,40 @@ +/** + * 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.lwm2m.server.rpc.composite; + +import org.eclipse.leshan.core.request.LwM2mRequest; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback; +import org.thingsboard.server.transport.lwm2m.server.rpc.RpcLwM2MDownlinkCallback; + +import java.util.Optional; + +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.contentToString; + +public class RpcObserveResponseCompositeCallback, T extends ObserveCompositeResponse> extends RpcLwM2MDownlinkCallback { + + public RpcObserveResponseCompositeCallback(TransportService transportService, LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, DownlinkRequestCallback callback) { + super(transportService, client, requestMsg, callback); + } + + @Override + protected Optional serializeSuccessfulResponse(T response) { + return contentToString(response.getContent()); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java new file mode 100644 index 0000000000..d44e6a95fe --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java @@ -0,0 +1,577 @@ +/** + * 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.lwm2m.server.store; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.eclipse.californium.core.coap.Token; +import org.eclipse.californium.core.network.TokenGenerator; +import org.eclipse.californium.core.network.TokenGenerator.Scope; +import org.eclipse.leshan.core.Destroyable; +import org.eclipse.leshan.core.Startable; +import org.eclipse.leshan.core.Stoppable; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.ObservationIdentifier; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.peer.LwM2mIdentity; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.util.NamedThreadFactory; +import org.eclipse.leshan.server.registration.Deregistration; +import org.eclipse.leshan.server.registration.ExpirationListener; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.registration.RegistrationStore; +import org.eclipse.leshan.server.registration.RegistrationUpdate; +import org.eclipse.leshan.server.registration.UpdatedRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import static org.eclipse.leshan.core.californium.ObserveUtil.CTX_CF_OBERSATION; +import static org.eclipse.leshan.core.californium.ObserveUtil.extractSerializedObservation; + +public class TbInMemoryRegistrationStore implements RegistrationStore, Startable, Stoppable, Destroyable { + private final Logger LOG = LoggerFactory.getLogger(TbInMemoryRegistrationStore.class); + + // Data structure + private final Map regsByEp = new HashMap<>(); + private final Map regsByAddr = new HashMap<>(); + private final Map regsByRegId = new HashMap<>(); + private final Map regsByIdentity = new HashMap<>(); + private final Map obsByToken = new HashMap<>(); + private final Map> tokensByRegId = new HashMap<>(); + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + // Listener use to notify when a registration expires + private ExpirationListener expirationListener; + + private final ScheduledExecutorService schedExecutor; + private ScheduledFuture cleanerTask; + private boolean started = false; + private final long cleanPeriod; // in seconds + + private final TokenGenerator tokenGenerator; + + private final LwM2mVersionedModelProvider modelProvider; + + public TbInMemoryRegistrationStore() { + this(null, 2, null); // default clean period : 2s + } + + public TbInMemoryRegistrationStore(TokenGenerator tokenGenerator, long cleanPeriodInSec, LwM2mVersionedModelProvider modelProvider) { + this(tokenGenerator, Executors.newScheduledThreadPool(1, + new NamedThreadFactory(String.format("TbInMemoryRegistrationStore Cleaner (%ds)", cleanPeriodInSec))), + cleanPeriodInSec, modelProvider); + } + + public TbInMemoryRegistrationStore(TokenGenerator tokenGenerator, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, LwM2mVersionedModelProvider modelProvider) { + this.schedExecutor = schedExecutor; + this.cleanPeriod = cleanPeriodInSec; + this.modelProvider = modelProvider; + this.tokenGenerator = tokenGenerator; + } + + /* *************** Leshan Registration API **************** */ + + @Override + public Deregistration addRegistration(Registration registration) { + try { + lock.writeLock().lock(); + + Registration registrationRemoved = regsByEp.put(registration.getEndpoint(), registration); + regsByRegId.put(registration.getId(), registration); + regsByIdentity.put(registration.getClientTransportData().getIdentity(), registration); + // If a registration is already associated to this address we don't care as we only want to keep the most + // recent binding. + regsByAddr.put(registration.getSocketAddress(), registration); + if (registrationRemoved != null) { + Collection observationsRemoved = unsafeRemoveAllObservations(registrationRemoved.getId()); + if (!registrationRemoved.getSocketAddress().equals(registration.getSocketAddress())) { + removeFromMap(regsByAddr, registrationRemoved.getSocketAddress(), registrationRemoved); + } + if (!registrationRemoved.getId().equals(registration.getId())) { + removeFromMap(regsByRegId, registrationRemoved.getId(), registrationRemoved); + } + if (!registrationRemoved.getClientTransportData().getIdentity() + .equals(registration.getClientTransportData().getIdentity())) { + removeFromMap(regsByIdentity, registrationRemoved.getClientTransportData().getIdentity(), + registrationRemoved); + } + return new Deregistration(registrationRemoved, observationsRemoved); + } + } finally { + lock.writeLock().unlock(); + } + return null; + } + + @Override + public UpdatedRegistration updateRegistration(RegistrationUpdate update) { + try { + lock.writeLock().lock(); + + Registration registration = getRegistration(update.getRegistrationId()); + if (registration == null) { + return null; + } else { + Registration updatedRegistration = update.update(registration); + regsByEp.put(updatedRegistration.getEndpoint(), updatedRegistration); + // If registration is already associated to this address we don't care as we only want to keep the most + // recent binding. + regsByAddr.put(updatedRegistration.getSocketAddress(), updatedRegistration); + if (!registration.getSocketAddress().equals(updatedRegistration.getSocketAddress())) { + removeFromMap(regsByAddr, registration.getSocketAddress(), registration); + } + regsByIdentity.put(updatedRegistration.getClientTransportData().getIdentity(), updatedRegistration); + if (!registration.getClientTransportData().getIdentity() + .equals(updatedRegistration.getClientTransportData().getIdentity())) { + removeFromMap(regsByIdentity, registration.getClientTransportData().getIdentity(), registration); + } + + regsByRegId.put(updatedRegistration.getId(), updatedRegistration); + + return new UpdatedRegistration(registration, updatedRegistration); + } + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public Registration getRegistration(String registrationId) { + try { + lock.readLock().lock(); + return regsByRegId.get(registrationId); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Registration getRegistrationByEndpoint(String endpoint) { + try { + lock.readLock().lock(); + return regsByEp.get(endpoint); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Registration getRegistrationByAdress(InetSocketAddress address) { + try { + lock.readLock().lock(); + return regsByAddr.get(address); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Registration getRegistrationByIdentity(LwM2mIdentity identity) { + try { + lock.readLock().lock(); + return regsByIdentity.get(identity); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Iterator getAllRegistrations() { + try { + lock.readLock().lock(); + return new ArrayList<>(regsByEp.values()).iterator(); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Deregistration removeRegistration(String registrationId) { + try { + lock.writeLock().lock(); + + Registration registration = getRegistration(registrationId); + if (registration != null) { + Collection observationsRemoved = unsafeRemoveAllObservations(registration.getId()); + regsByEp.remove(registration.getEndpoint()); + removeFromMap(regsByAddr, registration.getSocketAddress(), registration); + removeFromMap(regsByRegId, registration.getId(), registration); + removeFromMap(regsByIdentity, registration.getClientTransportData().getIdentity(), registration); + return new Deregistration(registration, observationsRemoved); + } + return null; + } finally { + lock.writeLock().unlock(); + } + } + + /* *************** Leshan Observation API **************** */ + + @Override + public Collection addObservation(String registrationId, Observation observation, boolean addIfAbsent) { + List removed = new ArrayList<>(); + try { + lock.writeLock().lock(); + + if (!regsByRegId.containsKey(registrationId)) { + throw new IllegalStateException(String.format( + "can not add observation %s there is no registration with id %s", observation, registrationId)); + } + + if (observation instanceof SingleObservation) { + if (validateObserveResource(((SingleObservation)observation).getPath(), registrationId)) { + updateSingleObservation(registrationId, observation, addIfAbsent, removed); + // cancel existing observations for the same path and registration id. + cancelObservation (observation, registrationId, removed); + } + } else { + ContentFormat ct = ((CompositeObservation) observation).getResponseContentFormat(); + Map ctx = observation.getContext(); + String serializedObservation = extractSerializedObservation(observation); + JsonNode nodeSerObs = JacksonUtil.toJsonNode(serializedObservation); + ((CompositeObservation)observation).getPaths().forEach(path -> { + if (validateObserveResource(path, registrationId)) { + String serializedObs = createSerializedSingleObservation(nodeSerObs, path.toString()); + Observation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs); + updateSingleObservation(registrationId, singleObservation, addIfAbsent, removed); + // cancel existing observations for the same path and registration id. + cancelObservation (singleObservation, registrationId, removed); + } + }); + } + + } finally { + lock.writeLock().unlock(); + } + + return removed; + } + + private boolean validateObserveResource(LwM2mPath path, String registrationId){ + // check if the resource is readable. + if (path.isResource() || path.isResourceInstance()) { + ObjectModel objectModel = modelProvider.getObjectModel(getRegistration(registrationId)).getObjectModel(path.getObjectId()); + ResourceModel resourceModel = objectModel == null ? null : objectModel.resources.get(path.getResourceId()); + if (resourceModel == null) { + return false; + } else if (!resourceModel.operations.isReadable()) { + return false; + } else if (path.isResourceInstance() && !resourceModel.multiple) { + return false; + } + } + return true; + } + + private void updateSingleObservation (String registrationId, Observation observation, boolean addIfAbsent, List removed) { + Observation previousObservation; + + ObservationIdentifier id = observation.getId(); + if (addIfAbsent) { + if (!obsByToken.containsKey(id)) + previousObservation = obsByToken.put(id, observation); + else + previousObservation = obsByToken.get(id); + + } else { + previousObservation = obsByToken.put(id, observation); + } + if (!tokensByRegId.containsKey(registrationId)) { + tokensByRegId.put(registrationId, new HashSet()); + } + tokensByRegId.get(registrationId).add(id); + + // log any collisions + if (previousObservation != null) { + removed.add(previousObservation); + LOG.warn("Token collision ? observation [{}] will be replaced by observation [{}] ", + previousObservation, observation); + } + } + + private Observation createSingleObservation(String registrationId, LwM2mPath target, ContentFormat ct, + Map ctx, String serializedObservation) { + + Token token = tokenGenerator.createToken(Scope.SHORT_TERM); + Map protocolData = Collections.emptyMap(); + if (serializedObservation != null) { + protocolData = new HashMap<>(); + protocolData.put(CTX_CF_OBERSATION, serializedObservation); + } + return new SingleObservation(new ObservationIdentifier(token.getBytes()), registrationId, target, ct, ctx, protocolData); + } + + private String createSerializedSingleObservation(JsonNode nodeSerObs, String path){ + if (nodeSerObs.has("context")){ + ((ObjectNode) nodeSerObs.get("context")).put("leshan-path", path + "\n"); + return JacksonUtil.toString(nodeSerObs); + } + return null; + } + + @Override + public Observation removeObservation(String registrationId, ObservationIdentifier observationId) { + try { + lock.writeLock().lock(); + Observation observation = unsafeGetObservation(observationId); + if (observation != null && registrationId.equals(observation.getRegistrationId())) { + unsafeRemoveObservation(observationId); + return observation; + } + return null; + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public Observation getObservation(String registrationId, ObservationIdentifier observationId) { + try { + lock.readLock().lock(); + Observation observation = unsafeGetObservation(observationId); + if (observation != null && registrationId.equals(observation.getRegistrationId())) { + return observation; + } + return null; + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Observation getObservation(ObservationIdentifier observationId) { + try { + lock.readLock().lock(); + Observation observation = unsafeGetObservation(observationId); + if (observation != null) { + return observation; + } + return null; + } finally { + lock.readLock().unlock(); + } + } + + /** + * Prepare for Cancel one Observation + * @param registrationId + * @return + */ + @Override + public Collection getObservations(String registrationId) { + try { + lock.readLock().lock(); + return unsafeGetObservations(registrationId); + } finally { + lock.readLock().unlock(); + } + } + + /** + * CancelAllObservation + * @param registrationId + * @return + */ + + @Override + public Collection removeObservations(String registrationId) { + try { + lock.writeLock().lock(); + return unsafeRemoveAllObservations(registrationId); + } finally { + lock.writeLock().unlock(); + } + } + + /* *************** Observation utility functions **************** */ + + private Observation unsafeGetObservation(ObservationIdentifier token) { + Observation obs = obsByToken.get(token); + return obs; + } + + private void cancelObservation (Observation observation, String registrationId, List removed) { + for (Observation obs : unsafeGetObservations(registrationId)) { + cancelExistingObservation(observation, obs, removed); + } + } + + private void cancelExistingObservation(Observation observation, Observation obs, List removed) { + LwM2mPath pathObservation = ((SingleObservation)observation).getPath(); + LwM2mPath pathObs = ((SingleObservation)obs).getPath(); + if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObservation = "3", pathObs = "3/0/9" + (pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) { + unsafeRemoveObservation(obs.getId()); + removed.add(obs); + } else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObservation = "3/0/9", pathObs = "3" + unsafeRemoveObservation(observation.getId()); + } + } + + private void unsafeRemoveObservation(ObservationIdentifier observationId) { + Observation removed = obsByToken.remove(observationId); + if (removed != null) { + String registrationId = removed.getRegistrationId(); + Set tokens = tokensByRegId.get(registrationId); + tokens.remove(observationId); + if (tokens.isEmpty()) { + tokensByRegId.remove(registrationId); + } + } + } + + + /** + * CancelAllObservation + * @param registrationId + * @return + */ + private Collection unsafeRemoveAllObservations(String registrationId) { + Collection removed = new ArrayList<>(); + Set ids = tokensByRegId.get(registrationId); + if (ids != null) { + for (ObservationIdentifier id : ids) { + Observation observationRemoved = obsByToken.remove(id); + if (observationRemoved != null) { + removed.add(observationRemoved); + } + } + } + tokensByRegId.remove(registrationId); + return removed; + } + + private Collection unsafeGetObservations(String registrationId) { + Collection result = new ArrayList<>(); + Set ids = tokensByRegId.get(registrationId); + if (ids != null) { + for (ObservationIdentifier id : ids) { + Observation obs = unsafeGetObservation(id); + if (obs != null) { + result.add(obs); + } + } + } + return result; + } + /* *************** Expiration handling **************** */ + + @Override + public void setExpirationListener(ExpirationListener listener) { + this.expirationListener = listener; + } + + /** + * start the registration store, will start regular cleanup of dead registrations. + */ + @Override + public synchronized void start() { + if (!started) { + started = true; + cleanerTask = schedExecutor.scheduleAtFixedRate(new TbInMemoryRegistrationStore.Cleaner(), cleanPeriod, cleanPeriod, TimeUnit.SECONDS); + } + } + + /** + * Stop the underlying cleanup of the registrations. + */ + @Override + public synchronized void stop() { + if (started) { + started = false; + if (cleanerTask != null) { + cleanerTask.cancel(false); + cleanerTask = null; + } + } + } + + /** + * Destroy "cleanup" scheduler. + */ + @Override + public synchronized void destroy() { + started = false; + schedExecutor.shutdownNow(); + try { + schedExecutor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + LOG.warn("Destroying InMemoryRegistrationStore was interrupted.", e); + } + } + + private class Cleaner implements Runnable { + + @Override + public void run() { + try { + Collection allRegs = new ArrayList<>(); + try { + lock.readLock().lock(); + allRegs.addAll(regsByEp.values()); + } finally { + lock.readLock().unlock(); + } + + for (Registration reg : allRegs) { + if (!reg.isAlive()) { + // force de-registration + Deregistration removedRegistration = removeRegistration(reg.getId()); + expirationListener.registrationExpired(removedRegistration.getRegistration(), + removedRegistration.getObservations()); + } + } + } catch (Exception e) { + LOG.warn("Unexpected Exception while registration cleaning", e); + } + } + } + + // boolean remove(Object key, Object value) exist only since java8 + // So this method is here only while we want to support java 7 + protected boolean removeFromMap(Map map, K key, V value) { + if (map.containsKey(key) && Objects.equals(map.get(key), value)) { + map.remove(key); + return true; + } else + return false; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index d8be30f86d..308cfe24ab 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -19,11 +19,11 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.Token; import org.eclipse.californium.core.network.serialization.UdpDataParser; import org.eclipse.californium.core.network.serialization.UdpDataSerializer; -import org.eclipse.californium.core.observe.ObservationStoreException; import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Stoppable; -import org.eclipse.leshan.core.californium.ObserveUtil; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.observation.ObservationIdentifier; import org.eclipse.leshan.core.observation.SingleObservation; @@ -82,6 +82,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab private static final String REG_EP_IDENTITY = "EP:IDENTITY:"; // secondary index key (Identity => Endpoint) private static final String LOCK_EP = "LOCK:EP:"; private static final byte[] OBS_TKN = "OBS:TKN:".getBytes(UTF_8); + private static final byte[] OBS_TKN_GET_ALL = "OBS:TKN:*".getBytes(UTF_8); private static final String OBS_TKNS_REGID_IDX = "TKNS:REGID:"; // secondary index (token list by registration) private static final byte[] EXP_EP = "EXP:EP".getBytes(UTF_8); // a sorted set used for registration expiration // (expiration date, Endpoint) @@ -477,16 +478,56 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab lock.lock(); // Add and Get previous observation - byte[] previousValue; + + byte[] previousValue = null; +// byte[] key; +// byte[] serializeObs; byte[] key = toKey(OBS_TKN, observation.getId().getBytes()); byte[] serializeObs = serializeObs(observation); - if (addIfAbsent) { + // we analyze the present previous value + Collection previousPaths = getPreviousPaths(observation, connection); + if (observation instanceof SingleObservation) { + if (previousPaths == null) { + connection.stringCommands().set(key, serializeObs); + } else if (!addIfAbsent) { + + } + + previousValue = connection.get(key); if (previousValue == null || previousValue.length == 0) { - connection.set(key, serializeObs); + if (observation instanceof CompositeObservation) { + List paths = ((CompositeObservation) observation).getPaths(); + if (paths.size()==1) { + key = toKey(OBS_TKN, observation.getId().getBytes()); + serializeObs = serializeObs(observation); + previousValue = connection.stringCommands().get(key); + if (previousValue == null || previousValue.length == 0) { +// connection.set(key, serializeObs); + connection.stringCommands().set(key, serializeObs); + } + paths.forEach(pObs -> { + + }); + } + else { + paths.forEach(pObs -> { + + }); + } + } else if (observation instanceof SingleObservation) { +// previousValue = connection.get(key); + previousValue = connection.stringCommands().get(key); + if (previousValue == null || previousValue.length == 0) { + connection.stringCommands().set(key, serializeObs); +// connection.set(key, serializeObs); + } + } } + } else { - previousValue = connection.getSet(key, serializeObs); +// previousValue = connection.getSet(key, serializeObs); + previousValue = connection.stringCommands().getSet(key, serializeObs); } // secondary index to get the list by registrationId @@ -501,11 +542,36 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab } // cancel existing observations for the same path and registration id. for (Observation obs : getObservations(connection, registrationId)) { - //TODO: should be able to use CompositeObservation - if (((SingleObservation)observation).getPath().equals(((SingleObservation)obs).getPath()) - && !observation.getId().equals(obs.getId())) { - removed.add(obs); - unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); + if (obs instanceof CompositeObservation){ + ((CompositeObservation)obs).getPaths().forEach(pObs -> { + if (observation instanceof CompositeObservation) { + ((CompositeObservation)observation).getPaths().forEach(pObservation -> { + //TODO include obs every (SingleObservation) in observation Composite + }); + log.info("observation obs -> CompositeObservation"); + } else if (observation instanceof SingleObservation) { + if (((SingleObservation) observation).getPath().equals(((SingleObservation)obs).getPath()) + && !observation.getId().equals(obs.getId())){ + removed.add(obs); + unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); + } + } + + }); + + } else if (obs instanceof SingleObservation) { + if (observation instanceof CompositeObservation) { + ((CompositeObservation)observation).getPaths().forEach(pObservation -> { + //TODO include obs every (SingleObservation) in observation Composite + }); + log.info("observation -> CompositeObservation, obs -> SingleObservation"); + } else if (observation instanceof SingleObservation) { + if (((SingleObservation) observation).getPath().equals(((SingleObservation)obs).getPath()) + && !observation.getId().equals(obs.getId())){ + removed.add(obs); + unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); + } + } } } } finally { @@ -573,10 +639,11 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab } } + private Collection getObservations(RedisConnection connection, String registrationId) { Collection result = new ArrayList<>(); - for (byte[] token : connection.lRange(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, -1)) { - byte[] obs = connection.get(toKey(OBS_TKN, token)); + for (byte[] token : connection.listCommands().lRange(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, -1)) { + byte[] obs = connection.stringCommands().get(toKey(OBS_TKN, token)); if (obs != null) { result.add(deserializeObs(obs)); } @@ -584,127 +651,156 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab return result; } - @Override - public Collection removeObservations(String registrationId) { - try (var connection = connectionFactory.getConnection()) { - // check registration exists - Registration registration = getRegistration(connection, registrationId); - if (registration == null) - return Collections.emptyList(); + private Collection getPreviousPaths(Observation observation, RedisConnection connection) { + Collection result = new ArrayList<>(); + Collection observations = getObservations(connection, observation.getRegistrationId()); + observations.forEach(obs -> { - // get endpoint and create lock - String endpoint = registration.getEndpoint(); - Lock lock = null; - String lockKey = toLockKey(endpoint); - try { - lock = redisLock.obtain(lockKey); - lock.lock(); - return unsafeRemoveAllObservations(connection, registrationId); - } finally { - if (lock != null) { - lock.unlock(); - } + Collection prevPath = getPreviousPaths( observation, obs); + if (prevPath != null){ + result.addAll(prevPath); } - } - } - - /* *************** Californium ObservationStore API **************** */ - - public org.eclipse.californium.core.observe.Observation putIfAbsent(Token token, - org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException { - return add(obs, true); + }); + return result.size() > 0 ? result : null; } - public org.eclipse.californium.core.observe.Observation put(Token token, - org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException { - return add(obs, false); - } - - private org.eclipse.californium.core.observe.Observation add(org.eclipse.californium.core.observe.Observation obs, boolean ifAbsent) throws ObservationStoreException { - String endpoint = ObserveUtil.validateCoapObservation(obs); - org.eclipse.californium.core.observe.Observation previousObservation = null; - - try (var connection = connectionFactory.getConnection()) { - Lock lock = null; - String lockKey = toLockKey(endpoint); - try { - lock = redisLock.obtain(lockKey); - lock.lock(); + private Collection getPreviousPaths(Observation observation, Observation prevObs) { + Collection prevPath = new ArrayList<>(); + if (observation instanceof SingleObservation && prevObs instanceof SingleObservation) { - String registrationId = ObserveUtil.extractRegistrationId(obs); - if (!connection.exists(toRegIdKey(registrationId))) - throw new ObservationStoreException("no registration for this Id"); - byte[] key = toKey(OBS_TKN, obs.getRequest().getToken().getBytes()); - Observation obsLeshan = buildLwM2mObservationFromCfToLeshanCore(obs); - byte[] serializeObs = serializeObs(obsLeshan); - byte[] previousValue; - if (ifAbsent) { - previousValue = connection.get(key); - if (previousValue == null || previousValue.length == 0) { - connection.set(key, serializeObs); - previousValue = connection.getSet(key, serializeObs); - } else { - return buildCoapObservationFromLeshanCoreToCfCore(deserializeObs(previousValue)); - } - } else { - previousValue = connection.getSet(key, serializeObs); - } + } else if (observation instanceof CompositeObservation) { + if (prevObs instanceof CompositeObservation) { // observation instanceof CompositeObservation && prevObs instanceof CompositeObservation - // secondary index to get the list by registrationId - connection.lPush(toKey(OBS_TKNS_REGID_IDX, registrationId), obs.getRequest().getToken().getBytes()); + } else { // observation instanceof CompositeObservation && prevObs instanceof SingleObservation - // log any collisions - if (previousValue != null && previousValue.length != 0) { - previousObservation = buildCoapObservationFromLeshanCoreToCfCore(deserializeObs(previousValue)); - LOG.warn( - "Token collision ? observation from request [{}] will be replaced by observation from request [{}] ", - previousObservation.getRequest(), obs.getRequest()); - } - } finally { - if (lock != null) { - lock.unlock(); - } } + } else { // observation instanceof SingleObservation prevObs instanceof CompositeObservation + } - return previousObservation; + return prevPath.size() > 0 ? prevPath : null; } - public void remove(Token token) { + @Override + public Collection removeObservations(String registrationId) { try (var connection = connectionFactory.getConnection()) { - byte[] tokenKey = toKey(OBS_TKN, token.getBytes()); - - // fetch the observation by token - byte[] serializedObs = connection.get(tokenKey); - if (serializedObs == null) - return; - -// org.eclipse.californium.core.observe.Observation obs = deserializeObs(serializedObs); - org.eclipse.californium.core.observe.Observation obs = null; - String registrationId = ObserveUtil.extractRegistrationId(obs); + // check registration exists Registration registration = getRegistration(connection, registrationId); - if (registration == null) { - LOG.warn("Unable to remove observation {}, registration {} does not exist anymore", obs.getRequest(), - registrationId); - return; - } + if (registration == null) + return Collections.emptyList(); + // get endpoint and create lock String endpoint = registration.getEndpoint(); Lock lock = null; String lockKey = toLockKey(endpoint); try { lock = redisLock.obtain(lockKey); lock.lock(); - - unsafeRemoveObservation(connection, registrationId, token.getBytes()); + return unsafeRemoveAllObservations(connection, registrationId); } finally { if (lock != null) { lock.unlock(); } } } - } + /* *************** Californium ObservationStore API **************** */ + +// public org.eclipse.californium.core.observe.Observation putIfAbsent(Token token, +// org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException { +// return add(obs, true); +// } + +// public org.eclipse.californium.core.observe.Observation put(Token token, +// org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException { +// return add(obs, false); +// } + +// private org.eclipse.californium.core.observe.Observation add(org.eclipse.californium.core.observe.Observation obs, boolean ifAbsent) throws ObservationStoreException { +// String endpoint = ObserveUtil.validateCoapObservation(obs); +// org.eclipse.californium.core.observe.Observation previousObservation = null; +// +// try (var connection = connectionFactory.getConnection()) { +// Lock lock = null; +// String lockKey = toLockKey(endpoint); +// try { +// lock = redisLock.obtain(lockKey); +// lock.lock(); +// +// String registrationId = ObserveUtil.extractRegistrationId(obs); +// if (!connection.exists(toRegIdKey(registrationId))) +// throw new ObservationStoreException("no registration for this Id"); +// byte[] key = toKey(OBS_TKN, obs.getRequest().getToken().getBytes()); +// Observation obsLeshan = buildLwM2mObservationFromCfToLeshanCore(obs); +// byte[] serializeObs = serializeObs(obsLeshan); +// byte[] previousValue; +// if (ifAbsent) { +// previousValue = connection.get(key); +// if (previousValue == null || previousValue.length == 0) { +// connection.set(key, serializeObs); +// previousValue = connection.getSet(key, serializeObs); +// } else { +// return buildCoapObservationFromLeshanCoreToCfCore(deserializeObs(previousValue)); +// } +// } else { +// previousValue = connection.getSet(key, serializeObs); +// } +// +// // secondary index to get the list by registrationId +// connection.lPush(toKey(OBS_TKNS_REGID_IDX, registrationId), obs.getRequest().getToken().getBytes()); +// +// // log any collisions +// if (previousValue != null && previousValue.length != 0) { +// previousObservation = buildCoapObservationFromLeshanCoreToCfCore(deserializeObs(previousValue)); +// LOG.warn( +// "Token collision ? observation from request [{}] will be replaced by observation from request [{}] ", +// previousObservation.getRequest(), obs.getRequest()); +// } +// } finally { +// if (lock != null) { +// lock.unlock(); +// } +// } +// } +// return previousObservation; +// } +// +// public void remove(Token token) { +// try (var connection = connectionFactory.getConnection()) { +// byte[] tokenKey = toKey(OBS_TKN, token.getBytes()); +// +// // fetch the observation by token +// byte[] serializedObs = connection.get(tokenKey); +// if (serializedObs == null) +// return; +// +//// org.eclipse.californium.core.observe.Observation obs = deserializeObs(serializedObs); +// org.eclipse.californium.core.observe.Observation obs = null; +// String registrationId = ObserveUtil.extractRegistrationId(obs); +// Registration registration = getRegistration(connection, registrationId); +// if (registration == null) { +// LOG.warn("Unable to remove observation {}, registration {} does not exist anymore", obs.getRequest(), +// registrationId); +// return; +// } +// +// String endpoint = registration.getEndpoint(); +// Lock lock = null; +// String lockKey = toLockKey(endpoint); +// try { +// lock = redisLock.obtain(lockKey); +// lock.lock(); +// +// unsafeRemoveObservation(connection, registrationId, token.getBytes()); +// } finally { +// if (lock != null) { +// lock.unlock(); +// } +// } +// } +// +// } + public Observation get(Token token) { try (var connection = connectionFactory.getConnection()) { byte[] obs = connection.get(toKey(OBS_TKN, token.getBytes())); @@ -745,15 +841,15 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab return observationSerDes.serialize(obs); } - private org.eclipse.leshan.core.observation.Observation buildLwM2mObservationFromCfToLeshanCore( - org.eclipse.californium.core.observe.Observation observation) { - String serializedObservation = observationSerDesCoap.serialize(observation); - return serializedObservation == null ? null : ObserveUtil.createLwM2mObservation(observation, serializedObservation); - } - private org.eclipse.californium.core.observe.Observation buildCoapObservationFromLeshanCoreToCfCore(Observation obs) { - String serializedObservation = ObserveUtil.extractSerializedObservation(obs); - return serializedObservation == null ? null : observationSerDesCoap.deserialize(serializedObservation); - } +// private Observation buildLwM2mObservationFromCfToLeshanCore( +// org.eclipse.californium.core.observe.Observation observation) { +// String serializedObservation = observationSerDesCoap.serialize(observation); +// return serializedObservation == null ? null : ObserveUtil.createLwM2mObservation(observation, serializedObservation); +// } +// private org.eclipse.californium.core.observe.Observation buildCoapObservationFromLeshanCoreToCfCore(Observation obs) { +// String serializedObservation = ObserveUtil.extractSerializedObservation(obs); +// return serializedObservation == null ? null : observationSerDesCoap.deserialize(serializedObservation); +// } private Observation deserializeObs(byte[] data) { return data == null ? null : observationSerDes.deserialize(data); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java index 9aaebaa8bc..016fe09c22 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java @@ -16,7 +16,7 @@ package org.thingsboard.server.transport.lwm2m.server.store; import lombok.RequiredArgsConstructor; -import org.eclipse.leshan.server.registration.InMemoryRegistrationStore; +import org.eclipse.californium.core.network.RandomTokenGenerator; import org.eclipse.leshan.server.registration.RegistrationStore; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -25,6 +25,7 @@ import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator; +import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; import java.util.Optional; @@ -37,10 +38,12 @@ public class TbLwM2mStoreFactory { private final LwM2MTransportServerConfig config; private final LwM2mCredentialsSecurityInfoValidator validator; + private final LwM2mVersionedModelProvider modelProvider; + @Bean private RegistrationStore registrationStore() { return redisConfiguration.isPresent() ? - new TbLwM2mRedisRegistrationStore(getConnectionFactory()) : new InMemoryRegistrationStore(config.getCleanPeriodInSec()); + new TbLwM2mRedisRegistrationStore(getConnectionFactory()) : new TbInMemoryRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), config.getCleanPeriodInSec(), modelProvider); } @Bean 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 84e1bfa53d..603128dbfc 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 jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -100,8 +102,6 @@ import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mSecurityStore; import org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -341,6 +341,8 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } else if (v instanceof LwM2mResource) { this.updateResourcesValue(lwM2MClient, (LwM2mResource) v, k.toString(), Mode.UPDATE, responseCode); } + } else { + this.onErrorObservation(registration, k + ": value in composite response is null"); } }); clientContext.update(lwM2MClient); @@ -348,6 +350,12 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } } + public void onErrorObservation(Registration registration, String errorMsg) { + LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(registration.getEndpoint()); + logService.log(lwM2MClient, LOG_LWM2M_ERROR + ": " + errorMsg); + } + + /** * Sending updated value to thingsboard from SendListener.dataReceived: object, instance, SingleResource or MultipleResource * @@ -557,7 +565,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl private void updateObjectInstanceResourceValue(LwM2mClient client, LwM2mObjectInstance lwM2mObjectInstance, String pathIdVer, int code) { LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer)); lwM2mObjectInstance.getResources().forEach((resourceId, resource) -> { - String pathRez = pathIds.toString() + "/" + resourceId; + String pathRez = pathIdVer + "/" + resourceId; this.updateResourcesValue(client, resource, pathRez, Mode.UPDATE, code); }); } @@ -600,7 +608,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl otaService.onCurrentSoftwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); } if (ResponseCode.BAD_REQUEST.getCode() > code) { - this.updateAttrTelemetry(registration, Collections.singleton(path)); + this.updateAttrTelemetry(registration, path); } } else { log.error("Fail update path [{}] Resource [{}]", path, lwM2mResource); @@ -617,9 +625,12 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * * @param registration - Registration LwM2M Client */ - private void updateAttrTelemetry(Registration registration, Set paths) { + public void updateAttrTelemetry(Registration registration, String path) { try { - ResultsAddKeyValueProto results = this.getParametersFromProfile(registration, paths); + ResultsAddKeyValueProto results = this.getParametersFromProfile(registration, path); + if (path.equals("/3_1.2/0/9")) { + log.info("UpdateTelemetry paths [{}] key: [{}] value [{}]", path, results.getResultTelemetries().get(0).getKey(), results.getResultTelemetries().get(0).getLongV()); + } SessionInfoProto sessionInfo = this.getSessionInfoOrCloseSession(registration); if (results != null && sessionInfo != null) { if (results.getResultAttributes().size() > 0) { @@ -669,13 +680,13 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * @param registration - Registration LwM2M Client * @param path - */ - private ResultsAddKeyValueProto getParametersFromProfile(Registration registration, Set path) { - if (path != null && path.size() > 0) { + private ResultsAddKeyValueProto getParametersFromProfile(Registration registration, String path) { + if (!path.isEmpty()) { ResultsAddKeyValueProto results = new ResultsAddKeyValueProto(); var profile = clientContext.getProfile(registration); List resultAttributes = new ArrayList<>(); profile.getObserveAttr().getAttribute().forEach(pathIdVer -> { - if (path.contains(pathIdVer)) { + if (path.equals(pathIdVer)) { TransportProtos.KeyValueProto kvAttr = this.getKvToThingsBoard(pathIdVer, registration); if (kvAttr != null) { resultAttributes.add(kvAttr); 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 9beebe5279..e95bb979ce 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 @@ -47,6 +47,7 @@ public interface LwM2mUplinkMsgHandler { void onUpdateValueAfterReadResponse(Registration registration, String path, ReadResponse response); void onUpdateValueAfterReadCompositeResponse(Registration registration, ReadCompositeResponse response); + void onErrorObservation(Registration registration, String errorMsg); void onUpdateValueWithSendRequest(Registration registration, SendRequest sendRequest); 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 201d41ce61..9f7003cbb3 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 @@ -79,7 +79,7 @@ public class LwM2MTransportUtil { public static final String LOG_LWM2M_INFO = "info"; public static final String LOG_LWM2M_ERROR = "error"; public static final String LOG_LWM2M_WARN = "warn"; - public static final int BOOTSTRAP_DEFAULT_SHORT_ID = 0; + public static final int BOOTSTRAP_DEFAULT_SHORT_ID_0 = 0; public enum LwM2MClientStrategy { CLIENT_STRATEGY_1(1, "Read only resources marked as observation"), @@ -223,6 +223,9 @@ public class LwM2MTransportUtil { public static String convertObjectIdToVersionedId(String path, Registration registration) { String ver = String.valueOf(registration.getSupportedObject().get(new LwM2mPath(path).getObjectId())); + return convertObjectIdToVerId(path, ver); + } + public static String convertObjectIdToVerId(String path, String ver) { ver = ver != null ? ver : TbLwM2mVersion.VERSION_1_0.getVersion().toString(); try { String[] keyArray = path.split(LWM2M_SEPARATOR_PATH); @@ -245,60 +248,6 @@ public class LwM2MTransportUtil { } } - /** - * As example: - * a)Write-Attributes/3/0/9?pmin=1 means the Battery Level value will be notified - * to the Server with a minimum interval of 1sec; - * this value is set at theResource level. - * b)Write-Attributes/3/0/9?pmin means the Battery Level will be notified - * to the Server with a minimum value (pmin) given by the default one - * (resource 2 of Object Server ID=1), - * or with another value if this Attribute has been set at another level - * (Object or Object Instance: see section5.1.1). - * c)Write-Attributes/3/0?pmin=10 means that all Resources of Instance 0 of the Object ‘Device (ID:3)’ - * will be notified to the Server with a minimum interval of 10 sec; - * this value is set at the Object Instance level. - * d)Write-Attributes /3/0/9?gt=45&st=10 means the Battery Level will be notified to the Server - * when: - * a.old value is 20 and new value is 35 due to step condition - * b.old value is 45 and new value is 50 due to gt condition - * c.old value is 50 and new value is 40 due to both gt and step conditions - * d.old value is 35 and new value is 20 due to step conditione) - * Write-Attributes /3/0/9?lt=20>=85&st=10 means the Battery Level will be notified to the Server - * when: - * a.old value is 17 and new value is 24 due to lt condition - * b.old value is 75 and new value is 90 due to both gt and step conditions - * String uriQueries = "pmin=10&pmax=60"; - * AttributeSet attributes = AttributeSet.parse(uriQueries); - * WriteAttributesRequest request = new WriteAttributesRequest(target, attributes); - * Attribute gt = new Attribute(GREATER_THAN, Double.valueOf("45")); - * Attribute st = new Attribute(LESSER_THAN, Double.valueOf("10")); - * Attribute pmax = new Attribute(MAXIMUM_PERIOD, "60"); - * Attribute [] attrs = {gt, st}; - */ -// 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, LwM2mUplinkMsgHandler serviceImpl, String target) { -// List attributeLists = new ArrayList<>(); -// Map map = JacksonUtil.convertValue(params, new TypeReference<>() { -// }); -// map.forEach((k, v) -> { -// if (StringUtils.trimToNull(v.toString()) != null) { -// Object attrValue = convertWriteAttributes(k, v, serviceImpl, target); -// if (attrValue != null) { -// Attribute attribute = createAttribute(k, attrValue); -// if (attribute != null) { -// attributeLists.add(new Attribute(k, attrValue)); -// } -// } -// } -// }); -// return attributeLists.toArray(Attribute[]::new); -// } - /** * "UNSIGNED_INTEGER": // Number -> Integer Example: * Alarm Timestamp [32-bit unsigned integer] @@ -357,41 +306,6 @@ public class LwM2MTransportUtil { STRING, type, new LwM2mPath(fromVersionedIdToObjectId(versionedId))); } -// public static Object convertWriteAttributes(String type, Object value, LwM2mUplinkMsgHandler serviceImpl, String target) { -// switch (type) { -// /** Integer [0:255]; */ -// case DIMENSION: -// 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.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), STRING, new LwM2mPath(target)); -// /**INTEGER */ -// case MINIMUM_PERIOD: -// case MAXIMUM_PERIOD: -// return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); -// /**Float; */ -// case GREATER_THAN: -// case LESSER_THAN: -// case STEP: -// if (value.getClass().getSimpleName().equals("String")) { -// value = Double.valueOf((String) value); -// } -// return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), FLOAT, new LwM2mPath(target)); -// default: -// return null; -// } -// } - -// private static Attribute createAttribute(String key, Object attrValue) { -// try { -// return new Attribute(key, attrValue); -// } catch (Exception e) { -// log.error("CreateAttribute, not valid parameter key: [{}], attrValue: [{}], error: [{}]", key, attrValue, e.getMessage()); -// return null; -// } -// } - /** * @param lwM2MClient - * @param path - From 221629fb29dc47059500007e1ea642e54dc4654d Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 16 Jan 2024 22:35:36 +0200 Subject: [PATCH 10/23] lwm2m: conflict resolution --- ...ntegrationDiscoverWriteAttributesTest.java | 23 ++++++++ ...pcLwm2mIntegrationWriteAttributesTest.java | 56 ------------------- 2 files changed, 23 insertions(+), 56 deletions(-) delete mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java index 536770cfd3..877433b0c2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java @@ -24,6 +24,7 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; @@ -134,6 +135,28 @@ public class RpcLwm2mIntegrationDiscoverWriteAttributesTest extends AbstractRpcL assertTrue(rpcActualResult.get("value").asText().contains(expected)); } + /** + * WriteAttributes {"id":"/3/0/14","attributes":{"pmax":100, "pmin":10}} + * if not implemented: + * {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"} + * if implemented: + * {"result":"CHANGED"} + */ + @Test + public void testWriteAttributesResourceWithParametersById_Result_CHANGED() throws Exception { + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; + String expectedValue = "{\"pmax\":100, \"pmin\":10}"; + String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + // result changed + actualResult = sendDiscover(expectedPath); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String expected = ";pmax=100;pmin=10"; + assertTrue(rpcActualResult.get("value").asText().contains(expected)); + } + /** * Class Attributes * Minimum/Maximum Period pmin/pmax diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java deleted file mode 100644 index b78758dc82..0000000000 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright © 2016-2024 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.lwm2m.rpc.sql; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.eclipse.leshan.core.ResponseCode; -import org.junit.Test; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; - - -public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest { - - /** - * WriteAttributes {"id":"/3/0/14","attributes":{"pmax":100, "pmin":10}} - * if not implemented: - * {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"} - * if implemented: - * {"result":"CHANGED"} - */ - @Test - public void testWriteAttributesResourceWithParametersById_Result_INTERNAL_SERVER_ERROR() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; - String expectedValue = "{\"pmax\":100, \"pmin\":10}"; - String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText()); - String expected = "not implemented"; - String actual = rpcActualResult.get("error").asText(); - assertTrue(actual.equals(expected)); - } - - private String sendRPCExecuteWithValueById(String path, String value) throws Exception { - String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}"; - return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); - } - -} From 4fe63cee703e64d4bf6f73493400b4df58712fae Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 17 Jan 2024 10:31:17 +0200 Subject: [PATCH 11/23] lwm2m: add exception to ObserveCompositeCancel --- .../observation/ObservationServiceImpl.java | 2 +- .../DefaultLwM2mAttributeParserTest.java | 2 +- .../DefaultLwM2mLinkParserTest.java | 2 +- .../lwm2m/attributes/LwM2mAttributesTest.java | 2 +- .../lwm2m/client/TbLwm2mObjectEnabler.java | 2 +- .../lwm2m/client/TbObjectsInitializer.java | 2 +- ...bstractRpcLwM2MIntegrationObserveTest.java | 2 +- ...cLwm2MIntegrationObserveCompositeTest.java | 43 ++++++++++++++- ...ntegrationDiscoverWriteAttributesTest.java | 2 +- .../sql/RpcLwm2mIntegrationObserveTest.java | 3 +- .../data/util/TbDefaultDDFFileValidator.java | 2 +- .../DefaultLwM2mDownlinkMsgHandler.java | 55 ++++++++++++------- ...TbLwM2MCancelObserveCompositeCallback.java | 2 +- .../TbLwM2MCancelObserveCompositeRequest.java | 2 +- .../TbLwM2MObserveCompositeCallback.java | 2 +- .../TbLwM2MObserveCompositeRequest.java | 2 +- .../RpcCancelObserveCompositeCallback.java | 2 +- .../RpcObserveResponseCompositeCallback.java | 2 +- .../store/TbInMemoryRegistrationStore.java | 2 +- 19 files changed, 96 insertions(+), 37 deletions(-) diff --git a/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java b/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java index 571b0e09bd..128c77d5b6 100644 --- a/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java +++ b/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mAttributeParserTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mAttributeParserTest.java index 0172710d0d..ede47667f5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mAttributeParserTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mAttributeParserTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java index 6f872432ec..cb293b5cdd 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/DefaultLwM2mLinkParserTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java index d77a3252c0..73f8346566 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java index 5fa55f24a3..f548cc30e2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java index 78322bcd7c..eff5ac962e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java index 63558fd489..3cda275e13 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java index ed7d71caee..86cd8186c7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. @@ -389,8 +389,49 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer3_0_9) + "\"]", actualValues); } + /** + * ObserveComposite {"ids":["/3", "/5/0/3", "/19/1/0/0"]} - Ok + * ObserveCompositeCancel {"ids":["/3/0/9", "/5/0/3", "/19/1/0/0"} -> BAD_REQUEST + * ObserveCompositeCancel {"ids":["/3"} -> CONTENT + */ + @Test + public void testObserveOneObjectAnyResources_Result_CONTENT_Cancel_OneResourceFromObjectAnyResource_Result_BAD_REQUEST_Cancel_OneObject_Result_CONTENT() throws Exception { + sendCancelObserveAllWithAwait(deviceId); + // ObserveComposite + sendCancelObserveAllWithAwait(deviceId); + String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; + String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; + String expectedIds = "[\"" + objectIdVer_3 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; + String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + + // ObserveCompositeCancel + expectedIds = "[\"" + expectedIdVer19_1_0_0 + "\", \"" + idVer_3_0_9 + "\"]"; + actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); + String expectedValue = "for observation path " + fromVersionedIdToObjectId(idVer_3_0_9) + ", that includes this observation path " + fromVersionedIdToObjectId(objectIdVer_3); + assertTrue(rpcActualResult.get("error").asText().contains(expectedValue)); + + // ObserveCompositeCancel + expectedIds = "[\"" + objectIdVer_3 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; + actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + assertEquals("2", rpcActualResult.get("value").asText()); + + String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); + ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); + String actualValues = rpcActualResultReadAll.get("value").asText(); + assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer5_0_3) + "\"]", actualValues); + } + + /** * ObserveComposite {"ids":["/3/0/9", "/3/0/14", "/5/0/3", "/3/0/15", "/19/1/0/0"]} - Ok + * ObserveCancel {"id":"/3/0/9"} -> INTERNAL_SERVER_ERROR * ObserveCompositeCancel {"ids":["/3/0/9", "/19/1/0/0", "/3]} - Ok * last Observation * @throws Exception diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java index 877433b0c2..d7807ee24a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index 77e8e2d0d8..b7de7f7dfc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -311,7 +311,8 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO /** * Observe {"id":"/3"} - * ObserveCancel {"id":"/3/0/9"} + * ObserveCancel {"id":"/3/0/9"} -> INTERNAL_SERVER_ERROR + * ObserveCancel {"id":"/3"} -> CONTENT */ @Test public void testObserveObject_ObserveCancelOneResource_Result_INTERNAL_SERVER_ERROR_Than_Cancel_ObserveObject_Result_CONTENT_Count_1() throws Exception { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDefaultDDFFileValidator.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDefaultDDFFileValidator.java index 36ed0f668d..b12bd5707e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDefaultDDFFileValidator.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDefaultDDFFileValidator.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. 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 2134ee5923..d8cd2b44ba 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 @@ -69,6 +69,8 @@ import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.eclipse.leshan.server.registration.Registration; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.device.profile.lwm2m.ObjectAttributes; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext; @@ -244,28 +246,43 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im @Override public void sendCancelObserveCompositeRequest(LwM2mClient client, TbLwM2MCancelObserveCompositeRequest request, DownlinkRequestCallback callback) { - Set observations = context.getServer().getObservationService().getObservations(client.getRegistration()); - List listPath = LwM2mPath.getLwM2mPathList(Arrays.asList(request.getObjectIds())); - Optional observationOpt = Optional.ofNullable(observations.stream().filter(observation -> observation instanceof CompositeObservation && ((CompositeObservation) observation).getPaths().equals(listPath)).findFirst().orElse(null)); - int cnt = 0; - if (observationOpt.isPresent()) { - cnt = context.getServer().getObservationService().cancelCompositeObservations(client.getRegistration(), request.getObjectIds()); - callback.onSuccess(request, cnt); - } else { - Set lwPaths = new HashSet<>(); - observations.forEach(obs -> { - LwM2mPath lwPathObs = ((SingleObservation) obs).getPath(); - listPath.forEach(nodePath -> { - if(nodePath.equals(lwPathObs) || lwPathObs.startWith(nodePath)){ // nodePath = "3", lwPathObs = "3/0/9": cancel for tne all lwPathObs - lwPaths.add(nodePath.toString()); + try { + Set observations = context.getServer().getObservationService().getObservations(client.getRegistration()); + List listPath = LwM2mPath.getLwM2mPathList(Arrays.asList(request.getObjectIds())); + Optional observationOpt = Optional.ofNullable(observations.stream().filter(observation -> observation instanceof CompositeObservation && ((CompositeObservation) observation).getPaths().equals(listPath)).findFirst().orElse(null)); + int cnt = 0; + if (observationOpt.isPresent()) { + cnt = context.getServer().getObservationService().cancelCompositeObservations(client.getRegistration(), request.getObjectIds()); + callback.onSuccess(request, cnt); + } else { + Set lwPaths = new HashSet<>(); + for (Observation obs : observations) { + LwM2mPath lwPathObs = ((SingleObservation) obs).getPath(); + for (LwM2mPath nodePath : listPath) { + String validNodePath = validatePathCompositeCancel(nodePath, lwPathObs, client); + if (validNodePath != null) lwPaths.add(validNodePath); } - }); - }); - for (String nodePath : lwPaths) { - cnt += context.getServer().getObservationService().cancelObservations(client.getRegistration(), nodePath); + }; + for (String nodePath : lwPaths) { + cnt += context.getServer().getObservationService().cancelObservations(client.getRegistration(), nodePath); + } } + callback.onSuccess(request, cnt); + } catch (ThingsboardException e){ + callback.onValidationError(request.toString(), e.getMessage()); + } + } + + private String validatePathCompositeCancel(LwM2mPath nodePath, LwM2mPath lwPathObs, LwM2mClient client) throws ThingsboardException { + if (nodePath.equals(lwPathObs) || lwPathObs.startWith(nodePath)) { // nodePath = "3", lwPathObs = "3/0/9": cancel for tne all lwPathObs + return nodePath.toString(); + } else if (!nodePath.equals(lwPathObs) && nodePath.startWith(lwPathObs)) { + String errorMsg = String.format( + "Unexpected error: There is registration with Endpoint %s for observation path %s, that includes this observation path %s", + client.getRegistration().getEndpoint(), nodePath, lwPathObs); + throw new ThingsboardException(errorMsg, ThingsboardErrorCode.BAD_REQUEST_PARAMS); } - callback.onSuccess(request, cnt); + return null; } @Override diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java index 9eb3797ea4..eb0f73be8f 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeCallback.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java index c3d4608bf5..5ff8e4dacf 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MCancelObserveCompositeRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java index 6b3855bb3c..2d76ef36da 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java index 93f9ddf726..c604226d69 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeRequest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.java index b011317ee3..ba29f4a1e2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcCancelObserveCompositeCallback.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java index 6768a70bdf..f0688dcc4f 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/composite/RpcObserveResponseCompositeCallback.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java index d44e6a95fe..3a8a73e478 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2023 The Thingsboard Authors + * Copyright © 2016-2024 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. From 3d495572c5b03befa6a7f678b514332198679297 Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 17 Jan 2024 18:28:21 +0200 Subject: [PATCH 12/23] lwm2m: add exception to ObserveCompositeCancel --- .../observation/ObservationServiceImpl.java | 4 +- .../sql/RpcLwm2mIntegrationObserveTest.java | 18 +- .../store/TbInMemoryRegistrationStore.java | 69 +++- .../store/TbLwM2mRedisRegistrationStore.java | 362 +++++++----------- .../server/store/TbLwM2mStoreFactory.java | 2 +- 5 files changed, 194 insertions(+), 261 deletions(-) diff --git a/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java b/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java index 128c77d5b6..dfd4259ec3 100644 --- a/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java +++ b/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java @@ -188,8 +188,8 @@ public class ObservationServiceImpl implements ObservationService, LwM2mNotifica result.add(obs); } else if (!lwPath.equals(lwPathObs) && lwPath.startWith(lwPathObs)) { // nodePath = "3/0/9", lwPathObs = "3": error... String errorMsg = String.format( - "Unexpected error: There is registration with id %s for observation path %s, that includes this observation path %s", - registrationId, lwPath, lwPathObs); + "Unexpected error : There is registration with id [%s] existing observation [%s] includes input observation [%s]!", + registrationId, lwPathObs, lwPath); throw new IllegalStateException(errorMsg); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index b7de7f7dfc..bf1ba41270 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -230,22 +230,22 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO /** * Observe {"id":["3"]} - Ok - * PreviousObservation contains "3/0/0" + * PreviousObservation contains "3/0/9" * @throws Exception */ @Test public void testObserve_Result_CONTENT_ONE_PATH_PreviousObservation_CONTAINCE_OTHER_CurrentObservation() throws Exception { sendCancelObserveAllWithAwait(deviceId); // "3/0/9" - String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; - String actualResult3_0_0 = sendRpcObserve("Observe", idVer_3_0_0); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult3_0_0, ObjectNode.class); + String idVer_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; + String actualResult3_0_9 = sendRpcObserve("Observe", idVer_3_0_9); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult3_0_9, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); // "3" String actualResult3 = sendRpcObserve("Observe", objectIdVer_3); rpcActualResult = JacksonUtil.fromString(actualResult3, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - // PreviousObservation "3/0/0" change to CurrentObservation "3" + // PreviousObservation "3/0/9" change to CurrentObservation "3" String actualResultReadAll = sendRpcObserve("ObserveReadAll", null); rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); @@ -263,16 +263,18 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO @Test public void testObserve_Result_CONTENT_ONE_PATH_CurrentObservation_CONTAINCE_OTHER_PreviousObservation() throws Exception { sendCancelObserveAllWithAwait(deviceId); + // "3" String actualResult3 = sendRpcObserve("Observe", objectIdVer_3); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult3, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - // "3/0/0" + + // "3/0/0"; WARN: - Token collision ? existing observation [/3] includes input observation [/3/0/0] String idVer_3_0_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; String actualResult3_0_0 = sendRpcObserve("Observe", idVer_3_0_0); rpcActualResult = JacksonUtil.fromString(actualResult3_0_0, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - // PreviousObservation "3" contains CurrentObservation "3/0/0" + String actualResultReadAll = sendRpcObserve("ObserveReadAll", null); rpcActualResult = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); @@ -331,7 +333,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO // cancel observe "/3_1.2/0/9" String expectedId_3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; String actualResult = sendRpcObserve("ObserveCancel", expectedId_3_0_9); - String expectedValue = "for observation path " + fromVersionedIdToObjectId(expectedId_3_0_9) + ", that includes this observation path " + fromVersionedIdToObjectId(objectIdVer_3); + String expectedValue = "existing observation [" + fromVersionedIdToObjectId(objectIdVer_3) + "] includes input observation [" + fromVersionedIdToObjectId(expectedId_3_0_9); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText()); assertTrue(rpcActualResult.get("error").asText().contains(expectedValue)); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java index 3a8a73e478..767eaf6b81 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java @@ -59,6 +59,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -258,8 +259,8 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable if (observation instanceof SingleObservation) { if (validateObserveResource(((SingleObservation)observation).getPath(), registrationId)) { - updateSingleObservation(registrationId, observation, addIfAbsent, removed); - // cancel existing observations for the same path and registration id. + updateSingleObservation(registrationId, (SingleObservation) observation, addIfAbsent, removed); + // cancel existing observations for the same path and registration id. cancelObservation (observation, registrationId, removed); } } else { @@ -270,8 +271,8 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable ((CompositeObservation)observation).getPaths().forEach(path -> { if (validateObserveResource(path, registrationId)) { String serializedObs = createSerializedSingleObservation(nodeSerObs, path.toString()); - Observation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs); - updateSingleObservation(registrationId, singleObservation, addIfAbsent, removed); + Observation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs, tokenGenerator); + updateSingleObservation(registrationId, (SingleObservation) singleObservation, addIfAbsent, removed); // cancel existing observations for the same path and registration id. cancelObservation (singleObservation, registrationId, removed); } @@ -301,35 +302,65 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable return true; } - private void updateSingleObservation (String registrationId, Observation observation, boolean addIfAbsent, List removed) { - Observation previousObservation; + private void updateSingleObservation (String registrationId, SingleObservation observation, boolean addIfAbsent, List removed) { + // Absorption by existing Observations + + + Observation previousObservation = null; + SingleObservation existingObservation = null; ObservationIdentifier id = observation.getId(); if (addIfAbsent) { - if (!obsByToken.containsKey(id)) - previousObservation = obsByToken.put(id, observation); - else - previousObservation = obsByToken.get(id); - + if (!obsByToken.containsKey(id)) { + existingObservation = validateByAbsorptionExistingObservations(observation); + if (existingObservation == null) { + obsByToken.put(id, observation); + } else if (!existingObservation.getPath().equals(observation.getPath())){ + obsByToken.put(id, observation); + previousObservation = obsByToken.get(existingObservation.getId()); + } + } } else { previousObservation = obsByToken.put(id, observation); } if (!tokensByRegId.containsKey(registrationId)) { tokensByRegId.put(registrationId, new HashSet()); } - tokensByRegId.get(registrationId).add(id); + + if (existingObservation == null || !existingObservation.getPath().equals(observation.getPath())) { + tokensByRegId.get(registrationId).add(id); + } // log any collisions - if (previousObservation != null) { - removed.add(previousObservation); - LOG.warn("Token collision ? observation [{}] will be replaced by observation [{}] ", - previousObservation, observation); + if (addIfAbsent && previousObservation != null) { + if (!existingObservation.getPath().equals(observation.getPath())) { + removed.add(previousObservation); + LOG.warn("Token collision ? observation [{}] will be replaced by observation [{}], that this observation includes input observation [{}]!", + previousObservation, observation, observation); + } else { + LOG.warn("Token collision ? existing observation [{}] includes input observation [{}]", + existingObservation, observation); + } } } - private Observation createSingleObservation(String registrationId, LwM2mPath target, ContentFormat ct, - Map ctx, String serializedObservation) { + private SingleObservation validateByAbsorptionExistingObservations (SingleObservation observation) { + LwM2mPath pathObservation = observation.getPath(); + AtomicReference result = new AtomicReference<>(); + obsByToken.values().stream().forEach(obs -> { + LwM2mPath pathObs = ((SingleObservation)obs).getPath(); + if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObs = "3/0/9"-> pathObservation = "3" + (pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) { + result.set((SingleObservation)obs); + } else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObs = "3" -> pathObservation = "3/0/9" + result.set(observation); + } + }); + return result.get(); + } + public static SingleObservation createSingleObservation(String registrationId, LwM2mPath target, ContentFormat ct, + Map ctx, String serializedObservation, TokenGenerator tokenGenerator) { Token token = tokenGenerator.createToken(Scope.SHORT_TERM); Map protocolData = Collections.emptyMap(); if (serializedObservation != null) { @@ -339,7 +370,7 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable return new SingleObservation(new ObservationIdentifier(token.getBytes()), registrationId, target, ct, ctx, protocolData); } - private String createSerializedSingleObservation(JsonNode nodeSerObs, String path){ + public static String createSerializedSingleObservation(JsonNode nodeSerObs, String path){ if (nodeSerObs.has("context")){ ((ObjectNode) nodeSerObs.get("context")).put("leshan-path", path + "\n"); return JacksonUtil.toString(nodeSerObs); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index f76528711d..3472d695f2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -15,19 +15,24 @@ */ package org.thingsboard.server.transport.lwm2m.server.store; +import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.Token; +import org.eclipse.californium.core.network.TokenGenerator; import org.eclipse.californium.core.network.serialization.UdpDataParser; import org.eclipse.californium.core.network.serialization.UdpDataSerializer; import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Stoppable; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.observation.ObservationIdentifier; import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.peer.LwM2mIdentity; +import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.util.NamedThreadFactory; import org.eclipse.leshan.core.util.Validate; import org.eclipse.leshan.server.redis.RedisRegistrationStore; @@ -47,6 +52,8 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.integration.redis.util.RedisLockRegistry; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; import java.net.InetSocketAddress; import java.util.ArrayList; @@ -56,14 +63,19 @@ import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.eclipse.leshan.core.californium.ObserveUtil.extractSerializedObservation; +import static org.thingsboard.server.transport.lwm2m.server.store.TbInMemoryRegistrationStore.createSerializedSingleObservation; +import static org.thingsboard.server.transport.lwm2m.server.store.TbInMemoryRegistrationStore.createSingleObservation; @Slf4j public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startable, Stoppable, Destroyable { @@ -106,24 +118,30 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab private final RedisLockRegistry redisLock; - public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory) { - this(connectionFactory, DEFAULT_CLEAN_PERIOD, DEFAULT_GRACE_PERIOD, DEFAULT_CLEAN_LIMIT); // default clean period 60s + private final TokenGenerator tokenGenerator; + + private final LwM2mVersionedModelProvider modelProvider; + + public TbLwM2mRedisRegistrationStore(TokenGenerator tokenGenerator,RedisConnectionFactory connectionFactory, LwM2mVersionedModelProvider modelProvider) { + this(tokenGenerator, connectionFactory, DEFAULT_CLEAN_PERIOD, DEFAULT_GRACE_PERIOD, DEFAULT_CLEAN_LIMIT, modelProvider); // default clean period 60s } - public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, long cleanPeriodInSec, long lifetimeGracePeriodInSec, int cleanLimit) { - this(connectionFactory, Executors.newScheduledThreadPool(1, + public TbLwM2mRedisRegistrationStore(TokenGenerator tokenGenerator, RedisConnectionFactory connectionFactory, long cleanPeriodInSec, long lifetimeGracePeriodInSec, int cleanLimit, LwM2mVersionedModelProvider modelProvider) { + this(tokenGenerator, connectionFactory, Executors.newScheduledThreadPool(1, new NamedThreadFactory(String.format("RedisRegistrationStore Cleaner (%ds)", cleanPeriodInSec))), - cleanPeriodInSec, lifetimeGracePeriodInSec, cleanLimit); + cleanPeriodInSec, lifetimeGracePeriodInSec, cleanLimit, modelProvider); } - public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, - long lifetimeGracePeriodInSec, int cleanLimit) { + public TbLwM2mRedisRegistrationStore(TokenGenerator tokenGenerator,RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, + long lifetimeGracePeriodInSec, int cleanLimit, LwM2mVersionedModelProvider modelProvider) { this.connectionFactory = connectionFactory; this.schedExecutor = schedExecutor; this.cleanPeriod = cleanPeriodInSec; this.cleanLimit = cleanLimit; this.gracePeriod = lifetimeGracePeriodInSec; this.redisLock = new RedisLockRegistry(connectionFactory, "Registration"); + this.tokenGenerator = tokenGenerator; + this.modelProvider = modelProvider; } /* *************** Redis Key utility function **************** */ @@ -464,7 +482,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab try (var connection = connectionFactory.getConnection()) { // fetch the client ep by registration ID index - byte[] ep = connection.get(toRegIdKey(registrationId)); + byte[] ep = connection.commands().get(toRegIdKey(registrationId)); if (ep == null) { throw new IllegalStateException(String.format( "can not add observation %s there is no registration with id %s", observation, registrationId)); @@ -476,103 +494,26 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab try { lock = redisLock.obtain(lockKey); lock.lock(); - - // Add and Get previous observation - - byte[] previousValue = null; -// byte[] key; -// byte[] serializeObs; - byte[] key = toKey(OBS_TKN, observation.getId().getBytes()); - byte[] serializeObs = serializeObs(observation); - // we analyze the present previous value - Collection previousPaths = getPreviousPaths(observation, connection); if (observation instanceof SingleObservation) { - if (previousPaths == null) { - connection.stringCommands().set(key, serializeObs); - } else if (!addIfAbsent) { - + if (validateObserveResource(((SingleObservation)observation).getPath(), registrationId)) { + updateSingleObservation(registrationId, (SingleObservation)observation, addIfAbsent, removed, connection); + // cancel existing observations for the same path and registration id. + cancelObservation(observation, registrationId, removed, connection); } - - - previousValue = connection.get(key); - if (previousValue == null || previousValue.length == 0) { - if (observation instanceof CompositeObservation) { - List paths = ((CompositeObservation) observation).getPaths(); - if (paths.size()==1) { - key = toKey(OBS_TKN, observation.getId().getBytes()); - serializeObs = serializeObs(observation); - previousValue = connection.stringCommands().get(key); - if (previousValue == null || previousValue.length == 0) { -// connection.set(key, serializeObs); - connection.stringCommands().set(key, serializeObs); - } - paths.forEach(pObs -> { - - }); - } - else { - paths.forEach(pObs -> { - - }); - } - } else if (observation instanceof SingleObservation) { -// previousValue = connection.get(key); - previousValue = connection.stringCommands().get(key); - if (previousValue == null || previousValue.length == 0) { - connection.stringCommands().set(key, serializeObs); -// connection.set(key, serializeObs); - } - } - } - } else { -// previousValue = connection.getSet(key, serializeObs); - previousValue = connection.stringCommands().getSet(key, serializeObs); - } - - // secondary index to get the list by registrationId - connection.lPush(toKey(OBS_TKNS_REGID_IDX, registrationId), observation.getId().getBytes()); - - // log any collisions - Observation previousObservation; - if (previousValue != null && previousValue.length != 0) { - previousObservation = deserializeObs(previousValue); - LOG.warn("Token collision ? observation [{}] will be replaced by observation [{}] ", - previousObservation, observation); - } - // cancel existing observations for the same path and registration id. - for (Observation obs : getObservations(connection, registrationId)) { - if (obs instanceof CompositeObservation){ - ((CompositeObservation)obs).getPaths().forEach(pObs -> { - if (observation instanceof CompositeObservation) { - ((CompositeObservation)observation).getPaths().forEach(pObservation -> { - //TODO include obs every (SingleObservation) in observation Composite - }); - log.info("observation obs -> CompositeObservation"); - } else if (observation instanceof SingleObservation) { - if (((SingleObservation) observation).getPath().equals(((SingleObservation)obs).getPath()) - && !observation.getId().equals(obs.getId())){ - removed.add(obs); - unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); - } - } - - }); - - } else if (obs instanceof SingleObservation) { - if (observation instanceof CompositeObservation) { - ((CompositeObservation)observation).getPaths().forEach(pObservation -> { - //TODO include obs every (SingleObservation) in observation Composite - }); - log.info("observation -> CompositeObservation, obs -> SingleObservation"); - } else if (observation instanceof SingleObservation) { - if (((SingleObservation) observation).getPath().equals(((SingleObservation)obs).getPath()) - && !observation.getId().equals(obs.getId())){ - removed.add(obs); - unsafeRemoveObservation(connection, registrationId, obs.getId().getBytes()); - } + ContentFormat ct = ((CompositeObservation) observation).getResponseContentFormat(); + Map ctx = observation.getContext(); + String serializedObservation = extractSerializedObservation(observation); + JsonNode nodeSerObs = JacksonUtil.toJsonNode(serializedObservation); + ((CompositeObservation)observation).getPaths().forEach(path -> { + if (validateObserveResource(path, registrationId)) { + String serializedObs = createSerializedSingleObservation(nodeSerObs, path.toString()); + SingleObservation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs, tokenGenerator); + updateSingleObservation(registrationId, singleObservation, addIfAbsent, removed, connection); + // cancel existing observations for the same path and registration id. + cancelObservation (singleObservation, registrationId, removed, connection); } - } + }); } } finally { if (lock != null) { @@ -583,6 +524,64 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab return removed; } + private boolean validateObserveResource(LwM2mPath path, String registrationId){ + // check if the resource is readable. + if (path.isResource() || path.isResourceInstance()) { + ObjectModel objectModel = modelProvider.getObjectModel(getRegistration(registrationId)).getObjectModel(path.getObjectId()); + ResourceModel resourceModel = objectModel == null ? null : objectModel.resources.get(path.getResourceId()); + if (resourceModel == null) { + return false; + } else if (!resourceModel.operations.isReadable()) { + return false; + } else if (path.isResourceInstance() && !resourceModel.multiple) { + return false; + } + } + return true; + } + + private void updateSingleObservation (String registrationId, SingleObservation observation, boolean addIfAbsent, + List removed, RedisConnection connection) { + + // Add and Get previous observation + byte[] previousValue; + byte[] key = toKey(OBS_TKN, observation.getId().getBytes()); + byte[] serializeObs = serializeObs(observation); + // we analyze the present previous value + SingleObservation existingObservation = null; + + if (addIfAbsent){ + previousValue = connection.stringCommands().get(key); + if (previousValue == null) { + existingObservation = validateByAbsorptionExistingObservations(observation, connection); + if (existingObservation == null){ + connection.stringCommands().set(key, serializeObs); + } else if(!existingObservation.getPath().equals(observation.getPath())) { + connection.stringCommands().set(key, serializeObs); + previousValue = serializeObs(existingObservation); + } + } + } else { + previousValue = connection.stringCommands().getSet(key, serializeObs); + } + + // secondary index to get the list by registrationId + connection.listCommands().lPush(toKey(OBS_TKNS_REGID_IDX, registrationId), observation.getId().getBytes()); + + // log any collisions + if (addIfAbsent && previousValue != null) { + if (!existingObservation.getPath().equals(observation.getPath())) { + Observation previousObservation = deserializeObs(previousValue); + removed.add(previousObservation); + LOG.warn("Token collision ? observation [{}] will be replaced by observation [{}], that this observation includes input observation [{}]!", + previousObservation, observation, observation); + } else { + LOG.warn("Token collision ? existing observation [{}] includes input observation [{}]", + existingObservation, observation); + } + } + } + @Override public Collection getObservations(String registrationId) { try (var connection = connectionFactory.getConnection()) { @@ -651,35 +650,25 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab return result; } - private Collection getPreviousPaths(Observation observation, RedisConnection connection) { - Collection result = new ArrayList<>(); + private SingleObservation validateByAbsorptionExistingObservations(SingleObservation observation, RedisConnection connection) { + LwM2mPath pathObservation = observation.getPath(); + AtomicReference result = new AtomicReference<>(); Collection observations = getObservations(connection, observation.getRegistrationId()); - observations.forEach(obs -> { - - Collection prevPath = getPreviousPaths( observation, obs); - if (prevPath != null){ - result.addAll(prevPath); + observations.stream().forEach(obs -> { + LwM2mPath pathObs = ((SingleObservation)obs).getPath(); + if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObs = "3/0/9"-> pathObservation = "3" + (pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) { + result.set((SingleObservation)obs); + } else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObs = "3" -> pathObservation = "3/0/9" + result.set(observation); } }); - return result.size() > 0 ? result : null; - } - - private Collection getPreviousPaths(Observation observation, Observation prevObs) { - Collection prevPath = new ArrayList<>(); - if (observation instanceof SingleObservation && prevObs instanceof SingleObservation) { - - } else if (observation instanceof CompositeObservation) { - if (prevObs instanceof CompositeObservation) { // observation instanceof CompositeObservation && prevObs instanceof CompositeObservation + return result.get(); - } else { // observation instanceof CompositeObservation && prevObs instanceof SingleObservation - } - } else { // observation instanceof SingleObservation prevObs instanceof CompositeObservation - - } - return prevPath.size() > 0 ? prevPath : null; } + @Override public Collection removeObservations(String registrationId) { try (var connection = connectionFactory.getConnection()) { @@ -704,103 +693,6 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab } } - /* *************** Californium ObservationStore API **************** */ - -// public org.eclipse.californium.core.observe.Observation putIfAbsent(Token token, -// org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException { -// return add(obs, true); -// } - -// public org.eclipse.californium.core.observe.Observation put(Token token, -// org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException { -// return add(obs, false); -// } - -// private org.eclipse.californium.core.observe.Observation add(org.eclipse.californium.core.observe.Observation obs, boolean ifAbsent) throws ObservationStoreException { -// String endpoint = ObserveUtil.validateCoapObservation(obs); -// org.eclipse.californium.core.observe.Observation previousObservation = null; -// -// try (var connection = connectionFactory.getConnection()) { -// Lock lock = null; -// String lockKey = toLockKey(endpoint); -// try { -// lock = redisLock.obtain(lockKey); -// lock.lock(); -// -// String registrationId = ObserveUtil.extractRegistrationId(obs); -// if (!connection.exists(toRegIdKey(registrationId))) -// throw new ObservationStoreException("no registration for this Id"); -// byte[] key = toKey(OBS_TKN, obs.getRequest().getToken().getBytes()); -// Observation obsLeshan = buildLwM2mObservationFromCfToLeshanCore(obs); -// byte[] serializeObs = serializeObs(obsLeshan); -// byte[] previousValue; -// if (ifAbsent) { -// previousValue = connection.get(key); -// if (previousValue == null || previousValue.length == 0) { -// connection.set(key, serializeObs); -// previousValue = connection.getSet(key, serializeObs); -// } else { -// return buildCoapObservationFromLeshanCoreToCfCore(deserializeObs(previousValue)); -// } -// } else { -// previousValue = connection.getSet(key, serializeObs); -// } -// -// // secondary index to get the list by registrationId -// connection.lPush(toKey(OBS_TKNS_REGID_IDX, registrationId), obs.getRequest().getToken().getBytes()); -// -// // log any collisions -// if (previousValue != null && previousValue.length != 0) { -// previousObservation = buildCoapObservationFromLeshanCoreToCfCore(deserializeObs(previousValue)); -// LOG.warn( -// "Token collision ? observation from request [{}] will be replaced by observation from request [{}] ", -// previousObservation.getRequest(), obs.getRequest()); -// } -// } finally { -// if (lock != null) { -// lock.unlock(); -// } -// } -// } -// return previousObservation; -// } -// -// public void remove(Token token) { -// try (var connection = connectionFactory.getConnection()) { -// byte[] tokenKey = toKey(OBS_TKN, token.getBytes()); -// -// // fetch the observation by token -// byte[] serializedObs = connection.get(tokenKey); -// if (serializedObs == null) -// return; -// -//// org.eclipse.californium.core.observe.Observation obs = deserializeObs(serializedObs); -// org.eclipse.californium.core.observe.Observation obs = null; -// String registrationId = ObserveUtil.extractRegistrationId(obs); -// Registration registration = getRegistration(connection, registrationId); -// if (registration == null) { -// LOG.warn("Unable to remove observation {}, registration {} does not exist anymore", obs.getRequest(), -// registrationId); -// return; -// } -// -// String endpoint = registration.getEndpoint(); -// Lock lock = null; -// String lockKey = toLockKey(endpoint); -// try { -// lock = redisLock.obtain(lockKey); -// lock.lock(); -// -// unsafeRemoveObservation(connection, registrationId, token.getBytes()); -// } finally { -// if (lock != null) { -// lock.unlock(); -// } -// } -// } -// -// } - public Observation get(Token token) { try (var connection = connectionFactory.getConnection()) { byte[] obs = connection.get(toKey(OBS_TKN, token.getBytes())); @@ -815,8 +707,8 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab /* *************** Observation utility functions **************** */ private void unsafeRemoveObservation(RedisConnection connection, String registrationId, byte[] observationId) { - if (connection.del(toKey(OBS_TKN, observationId)) > 0L) { - connection.lRem(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, observationId); + if (connection.commands().del(toKey(OBS_TKN, observationId)) > 0L) { + connection.listCommands().lRem(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, observationId); } } @@ -841,15 +733,23 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab return observationSerDes.serialize(obs); } -// private Observation buildLwM2mObservationFromCfToLeshanCore( -// org.eclipse.californium.core.observe.Observation observation) { -// String serializedObservation = observationSerDesCoap.serialize(observation); -// return serializedObservation == null ? null : ObserveUtil.createLwM2mObservation(observation, serializedObservation); -// } -// private org.eclipse.californium.core.observe.Observation buildCoapObservationFromLeshanCoreToCfCore(Observation obs) { -// String serializedObservation = ObserveUtil.extractSerializedObservation(obs); -// return serializedObservation == null ? null : observationSerDesCoap.deserialize(serializedObservation); -// } + private void cancelObservation(Observation observation, String registrationId, List removed, RedisConnection connection) { + for (Observation obs : getObservations(connection, registrationId)) { + cancelExistingObservation(connection, observation, obs, removed); + } + } + + private void cancelExistingObservation(RedisConnection connection, Observation observation, Observation obs, List removed) { + LwM2mPath pathObservation = ((SingleObservation)observation).getPath(); + LwM2mPath pathObs = ((SingleObservation)obs).getPath(); + if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObservation = "3", pathObs = "3/0/9" + (pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) { + unsafeRemoveObservation(connection, obs.getRegistrationId(), obs.getId().getBytes()); + removed.add(obs); + } else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObservation = "3/0/9", pathObs = "3" + unsafeRemoveObservation(connection, obs.getRegistrationId(), observation.getId().getBytes()); + } + } private Observation deserializeObs(byte[] data) { return data == null ? null : observationSerDes.deserialize(data); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java index 7dd921f07c..24df40e378 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java @@ -43,7 +43,7 @@ public class TbLwM2mStoreFactory { @Bean private RegistrationStore registrationStore() { return redisConfiguration.isPresent() ? - new TbLwM2mRedisRegistrationStore(getConnectionFactory()) : new TbInMemoryRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), config.getCleanPeriodInSec(), modelProvider); + new TbLwM2mRedisRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), getConnectionFactory(), modelProvider) : new TbInMemoryRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), config.getCleanPeriodInSec(), modelProvider); } @Bean From ad46f3bb2be70b05f8a8d537f62c3e5702c3f845 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 18 Jan 2024 11:30:04 +0200 Subject: [PATCH 13/23] lwm2m: Rebuild --- .../lwm2m/server/store/TbInMemoryRegistrationStore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java index 767eaf6b81..d6ef855765 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java @@ -351,7 +351,7 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable LwM2mPath pathObs = ((SingleObservation)obs).getPath(); if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObs = "3/0/9"-> pathObservation = "3" (pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) { - result.set((SingleObservation)obs); + result.set((SingleObservation)obs); } else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObs = "3" -> pathObservation = "3/0/9" result.set(observation); } From a74640a1d8b3e0511e482cba986d3d551b621139 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 18 Jan 2024 13:31:28 +0200 Subject: [PATCH 14/23] lwm2m: fix bug test by ver Object, fix buf test: TbLwM2mStoreFactory.class "NetworkConfig must not be null" --- .../server/controller/TbResourceControllerTest.java | 2 +- .../transport/lwm2m/server/store/TbLwM2mStoreFactory.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index 83da76344c..1d875d2ccc 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -687,7 +687,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { List resources = loadLwm2mResources(); List objects = - doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.0,5_1.0,19_1.1", new TypeReference<>() {}); + doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.2,5_1.2,19_1.1", new TypeReference<>() {}); Assert.assertNotNull(objects); Assert.assertEquals(3, objects.size()); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java index 24df40e378..852a022ccb 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java @@ -19,6 +19,7 @@ import lombok.RequiredArgsConstructor; import org.eclipse.californium.core.network.RandomTokenGenerator; import org.eclipse.leshan.server.registration.RegistrationStore; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Lazy; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.TBRedisCacheConfiguration; @@ -35,6 +36,7 @@ import java.util.Optional; public class TbLwM2mStoreFactory { private final Optional redisConfiguration; + @Lazy private final LwM2MTransportServerConfig config; private final LwM2mCredentialsSecurityInfoValidator validator; @@ -43,7 +45,8 @@ public class TbLwM2mStoreFactory { @Bean private RegistrationStore registrationStore() { return redisConfiguration.isPresent() ? - new TbLwM2mRedisRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), getConnectionFactory(), modelProvider) : new TbInMemoryRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), config.getCleanPeriodInSec(), modelProvider); + new TbLwM2mRedisRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), getConnectionFactory(), modelProvider) : + new TbInMemoryRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), config.getCleanPeriodInSec(), modelProvider); } @Bean From 92353e28208c6bf032d2e5303cb628c4aa2643ea Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 18 Jan 2024 17:40:39 +0200 Subject: [PATCH 15/23] lwm2m: add logs1 -> fix bug test by ver Object, fix buf test: TbLwM2mStoreFactory.class "NetworkConfig must not be null" --- .../transport/lwm2m/server/store/TbLwM2mStoreFactory.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java index 852a022ccb..cc9bb980d8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.server.store; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.network.RandomTokenGenerator; import org.eclipse.leshan.server.registration.RegistrationStore; import org.springframework.context.annotation.Bean; @@ -30,6 +31,7 @@ import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider import java.util.Optional; +@Slf4j @Component @TbLwM2mTransportComponent @RequiredArgsConstructor @@ -44,6 +46,11 @@ public class TbLwM2mStoreFactory { @Bean private RegistrationStore registrationStore() { + if (config == null || config.getCoapConfig() == null) { + log.error("For the test: CoapConfig is null!"); + } else { + log.info("For the test: CoapConfig: is ok!"); + } return redisConfiguration.isPresent() ? new TbLwM2mRedisRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), getConnectionFactory(), modelProvider) : new TbInMemoryRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), config.getCleanPeriodInSec(), modelProvider); From abb907b34260fbfc94ff92c774f340ed60fbd554 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 18 Jan 2024 18:15:39 +0200 Subject: [PATCH 16/23] lwm2m: fix bug2: TbLwM2mStoreFactory.class "NetworkConfig must not be null" --- .../store/TbInMemoryRegistrationStore.java | 23 +++++++++++----- .../store/TbLwM2mRedisRegistrationStore.java | 26 +++++++++++++------ .../server/store/TbLwM2mStoreFactory.java | 13 ++-------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java index d6ef855765..a9f65ff0bf 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbInMemoryRegistrationStore.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.lwm2m.server.store; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.eclipse.californium.core.coap.Token; +import org.eclipse.californium.core.network.RandomTokenGenerator; import org.eclipse.californium.core.network.TokenGenerator; import org.eclipse.californium.core.network.TokenGenerator.Scope; import org.eclipse.leshan.core.Destroyable; @@ -42,6 +43,7 @@ import org.eclipse.leshan.server.registration.UpdatedRegistration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; import java.net.InetSocketAddress; @@ -87,25 +89,27 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable private boolean started = false; private final long cleanPeriod; // in seconds - private final TokenGenerator tokenGenerator; + private final LwM2MTransportServerConfig config; private final LwM2mVersionedModelProvider modelProvider; + private TokenGenerator tokenGenerator; + public TbInMemoryRegistrationStore() { this(null, 2, null); // default clean period : 2s } - public TbInMemoryRegistrationStore(TokenGenerator tokenGenerator, long cleanPeriodInSec, LwM2mVersionedModelProvider modelProvider) { - this(tokenGenerator, Executors.newScheduledThreadPool(1, + public TbInMemoryRegistrationStore(LwM2MTransportServerConfig config, long cleanPeriodInSec, LwM2mVersionedModelProvider modelProvider) { + this(config, Executors.newScheduledThreadPool(1, new NamedThreadFactory(String.format("TbInMemoryRegistrationStore Cleaner (%ds)", cleanPeriodInSec))), cleanPeriodInSec, modelProvider); } - public TbInMemoryRegistrationStore(TokenGenerator tokenGenerator, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, LwM2mVersionedModelProvider modelProvider) { + public TbInMemoryRegistrationStore(LwM2MTransportServerConfig config, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, LwM2mVersionedModelProvider modelProvider) { this.schedExecutor = schedExecutor; this.cleanPeriod = cleanPeriodInSec; this.modelProvider = modelProvider; - this.tokenGenerator = tokenGenerator; + this.config = config; } /* *************** Leshan Registration API **************** */ @@ -271,7 +275,7 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable ((CompositeObservation)observation).getPaths().forEach(path -> { if (validateObserveResource(path, registrationId)) { String serializedObs = createSerializedSingleObservation(nodeSerObs, path.toString()); - Observation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs, tokenGenerator); + Observation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs, getTokenGenerator()); updateSingleObservation(registrationId, (SingleObservation) singleObservation, addIfAbsent, removed); // cancel existing observations for the same path and registration id. cancelObservation (singleObservation, registrationId, removed); @@ -359,6 +363,13 @@ public class TbInMemoryRegistrationStore implements RegistrationStore, Startable return result.get(); } + private TokenGenerator getTokenGenerator(){ + if (this.tokenGenerator == null) { + this.tokenGenerator = new RandomTokenGenerator(config.getCoapConfig()); + } + return this.tokenGenerator; + } + public static SingleObservation createSingleObservation(String registrationId, LwM2mPath target, ContentFormat ct, Map ctx, String serializedObservation, TokenGenerator tokenGenerator) { Token token = tokenGenerator.createToken(Scope.SHORT_TERM); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index 3472d695f2..898fef7b2f 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.lwm2m.server.store; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.Token; +import org.eclipse.californium.core.network.RandomTokenGenerator; import org.eclipse.californium.core.network.TokenGenerator; import org.eclipse.californium.core.network.serialization.UdpDataParser; import org.eclipse.californium.core.network.serialization.UdpDataSerializer; @@ -53,6 +54,7 @@ import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.integration.redis.util.RedisLockRegistry; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; import java.net.InetSocketAddress; @@ -118,21 +120,22 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab private final RedisLockRegistry redisLock; - private final TokenGenerator tokenGenerator; + private final LwM2MTransportServerConfig config; + private TokenGenerator tokenGenerator; private final LwM2mVersionedModelProvider modelProvider; - public TbLwM2mRedisRegistrationStore(TokenGenerator tokenGenerator,RedisConnectionFactory connectionFactory, LwM2mVersionedModelProvider modelProvider) { - this(tokenGenerator, connectionFactory, DEFAULT_CLEAN_PERIOD, DEFAULT_GRACE_PERIOD, DEFAULT_CLEAN_LIMIT, modelProvider); // default clean period 60s + public TbLwM2mRedisRegistrationStore(LwM2MTransportServerConfig config, RedisConnectionFactory connectionFactory, LwM2mVersionedModelProvider modelProvider) { + this(config, connectionFactory, DEFAULT_CLEAN_PERIOD, DEFAULT_GRACE_PERIOD, DEFAULT_CLEAN_LIMIT, modelProvider); // default clean period 60s } - public TbLwM2mRedisRegistrationStore(TokenGenerator tokenGenerator, RedisConnectionFactory connectionFactory, long cleanPeriodInSec, long lifetimeGracePeriodInSec, int cleanLimit, LwM2mVersionedModelProvider modelProvider) { - this(tokenGenerator, connectionFactory, Executors.newScheduledThreadPool(1, + public TbLwM2mRedisRegistrationStore(LwM2MTransportServerConfig config, RedisConnectionFactory connectionFactory, long cleanPeriodInSec, long lifetimeGracePeriodInSec, int cleanLimit, LwM2mVersionedModelProvider modelProvider) { + this(config, connectionFactory, Executors.newScheduledThreadPool(1, new NamedThreadFactory(String.format("RedisRegistrationStore Cleaner (%ds)", cleanPeriodInSec))), cleanPeriodInSec, lifetimeGracePeriodInSec, cleanLimit, modelProvider); } - public TbLwM2mRedisRegistrationStore(TokenGenerator tokenGenerator,RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, + public TbLwM2mRedisRegistrationStore(LwM2MTransportServerConfig config, RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, long lifetimeGracePeriodInSec, int cleanLimit, LwM2mVersionedModelProvider modelProvider) { this.connectionFactory = connectionFactory; this.schedExecutor = schedExecutor; @@ -140,7 +143,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab this.cleanLimit = cleanLimit; this.gracePeriod = lifetimeGracePeriodInSec; this.redisLock = new RedisLockRegistry(connectionFactory, "Registration"); - this.tokenGenerator = tokenGenerator; + this.config = config; this.modelProvider = modelProvider; } @@ -508,7 +511,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab ((CompositeObservation)observation).getPaths().forEach(path -> { if (validateObserveResource(path, registrationId)) { String serializedObs = createSerializedSingleObservation(nodeSerObs, path.toString()); - SingleObservation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs, tokenGenerator); + SingleObservation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs, getTokenGenerator()); updateSingleObservation(registrationId, singleObservation, addIfAbsent, removed, connection); // cancel existing observations for the same path and registration id. cancelObservation (singleObservation, registrationId, removed, connection); @@ -706,6 +709,13 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab /* *************** Observation utility functions **************** */ + private TokenGenerator getTokenGenerator(){ + if (this.tokenGenerator == null) { + this.tokenGenerator = new RandomTokenGenerator(config.getCoapConfig()); + } + return this.tokenGenerator; + } + private void unsafeRemoveObservation(RedisConnection connection, String registrationId, byte[] observationId) { if (connection.commands().del(toKey(OBS_TKN, observationId)) > 0L) { connection.listCommands().lRem(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, observationId); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java index cc9bb980d8..77755abb33 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java @@ -17,10 +17,8 @@ package org.thingsboard.server.transport.lwm2m.server.store; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.network.RandomTokenGenerator; import org.eclipse.leshan.server.registration.RegistrationStore; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Lazy; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.TBRedisCacheConfiguration; @@ -38,22 +36,15 @@ import java.util.Optional; public class TbLwM2mStoreFactory { private final Optional redisConfiguration; - @Lazy private final LwM2MTransportServerConfig config; private final LwM2mCredentialsSecurityInfoValidator validator; - private final LwM2mVersionedModelProvider modelProvider; @Bean private RegistrationStore registrationStore() { - if (config == null || config.getCoapConfig() == null) { - log.error("For the test: CoapConfig is null!"); - } else { - log.info("For the test: CoapConfig: is ok!"); - } return redisConfiguration.isPresent() ? - new TbLwM2mRedisRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), getConnectionFactory(), modelProvider) : - new TbInMemoryRegistrationStore(new RandomTokenGenerator(config.getCoapConfig()), config.getCleanPeriodInSec(), modelProvider); + new TbLwM2mRedisRegistrationStore(config, getConnectionFactory(), modelProvider) : + new TbInMemoryRegistrationStore(config, config.getCleanPeriodInSec(), modelProvider); } @Bean From 7a173f98bf7dbcd712718cb8d6d9c994c8d85c99 Mon Sep 17 00:00:00 2001 From: nick Date: Sun, 21 Jan 2024 15:50:31 +0200 Subject: [PATCH 17/23] lwm2m: comments1 --- .../observation/ObservationServiceImpl.java | 15 + common/transport/lwm2m/pom.xml | 2 +- .../LwM2MTransportBootstrapService.java | 7 +- .../store/LwM2MConfigurationChecker.java | 4 +- ...LwM2mCredentialsSecurityInfoValidator.java | 2 +- .../server/DefaultLwM2mTransportService.java | 25 +- .../lwm2m/server/LwM2MNetworkConfig.java | 1 - .../lwm2m/server/LwM2mServerListener.java | 2 +- .../lwm2m/server/LwM2mSessionMsgListener.java | 2 +- pom.xml | 2 +- .../observation/ObservationServiceImpl.java | 296 ++++++++++++++++++ 11 files changed, 332 insertions(+), 26 deletions(-) create mode 100644 transport/lwm2m/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java diff --git a/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java b/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java index dfd4259ec3..89a2f07179 100644 --- a/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java +++ b/application/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java @@ -12,6 +12,21 @@ * 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. + * + * Copyright (c) 2016 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + * Michał Wadowski (Orange) - Add Observe-Composite feature. */ package org.eclipse.leshan.server.observation; diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index fcd6678071..e69e4d54ac 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -97,7 +97,7 @@ org.awaitility awaitility - 4.2.0 + test org.eclipse.californium diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index bf72d8859d..d6839ae394 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -113,13 +113,8 @@ public class LwM2MTransportBootstrapService { serverCoapConfig.setTransient(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY); serverCoapConfig.set(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, serverConfig.isRecommendedCiphers()); serverCoapConfig.setTransient(DtlsConfig.DTLS_CONNECTION_ID_LENGTH); - int cid = 6; - if (cid > 0) { - serverCoapConfig.set(DtlsConfig.DTLS_CONNECTION_ID_LENGTH, cid); - } - + serverCoapConfig.set(DtlsConfig.DTLS_CONNECTION_ID_LENGTH, 6); serverCoapConfig.set(DTLS_RECOMMENDED_CURVES_ONLY, serverConfig.isRecommendedSupportedGroups()); - serverCoapConfig.setTransient(DTLS_RETRANSMISSION_TIMEOUT); serverCoapConfig.set(DTLS_RETRANSMISSION_TIMEOUT, serverConfig.getDtlsRetransmissionTimeout(), MILLISECONDS); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java index 94633184d9..12283f2aff 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java @@ -63,12 +63,12 @@ public class LwM2MConfigurationChecker extends ConfigurationChecker { if (security == null) { throw new InvalidConfigurationException("no security entry for server instance: " + e.getKey()); } - // BS Server + // BS Server if (security.bootstrapServer && srvCfg.shortId != 0) { throw new InvalidConfigurationException("short ID must be 0"); } - // LwM2M Server + // LwM2M Server /** * This identifier uniquely identifies each LwM2M Server configured for the LwM2M Client. * This Resource MUST be set when the Bootstrap-Server Resource has false value. diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java index 9ce05dec13..a44c6adb6e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java @@ -67,7 +67,7 @@ public class LwM2mCredentialsSecurityInfoValidator { new TransportServiceCallback<>() { @Override public void onSuccess(ValidateDeviceCredentialsResponse msg) { - log.info("Validated credentials: [{}] [{}]", credentialsId, msg); + log.trace("Validated credentials: [{}] [{}]", credentialsId, msg); resultSecurityStore[0] = createSecurityInfo(credentialsId, msg, keyValue); latch.countDown(); } 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 68f807125c..27bf6251a9 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 @@ -68,7 +68,6 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { public static final CipherSuite[] RPK_OR_X509_CIPHER_SUITES = {TLS_PSK_WITH_AES_128_CCM_8, TLS_PSK_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256}; public static final CipherSuite[] PSK_CIPHER_SUITES = {TLS_PSK_WITH_AES_128_CCM_8, TLS_PSK_WITH_AES_128_CBC_SHA256}; -// public static final CipherSuite[] PSK_CIPHER_SUITES = {TLS_PSK_WITH_AES_128_CCM_8}; private final LwM2mTransportContext context; private final LwM2MTransportServerConfig config; @@ -92,8 +91,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { * nameFile = "BC68JAR01A09_TO_BC68JAR01A10.bin" * "coap://host:port/{path}/{token}/{nameFile}" */ - LwM2mTransportCoapResource otaCoapResource = new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE); -// this.server.coap().getServer().add(otaCoapResource); + new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE); this.context.setServer(server); this.startLhServer(); } @@ -168,16 +166,19 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { serverCoapConfig.set(DTLS_ROLE, SERVER_ONLY); serverCoapConfig.setTransient(DtlsConfig.DTLS_CONNECTION_ID_LENGTH); /** - * "Control usage of DTLS connection ID.", // - * "- 'on' to activate Connection ID support ", // - * " (same as -cid 6)", // - * "- 'off' to deactivate it", // - * "- Positive value define the size in byte of CID generated.", // - * "- 0 value means we accept to use CID but will not generated one for foreign peer.", // - * "Default: on" + * "Control usage of DTLS connection ID.", + * If DTLS_CONNECTION_ID_LENGTH enables the use of a connection id, this node id could be used + * to configure the generation of connection ids specific for node in a multi-node deployment (cluster). + * The value is used as first byte in generated connection ids. + * DTLS connection ID length. disabled, + * 0 enables support without active use of CID.", defaultValue == null, minimumValue == 0); + * "- 'on' to activate Connection ID support ", + * " (same as -cid 6)", + * "- 'off' to deactivate it", + * "- Positive value define the size in byte of CID generated.", + * "- 0 value means we accept to use CID but will not generated one for foreign peer.", */ - int cid = 6; - serverCoapConfig.set(DtlsConfig.DTLS_CONNECTION_ID_LENGTH, cid); + serverCoapConfig.set(DtlsConfig.DTLS_CONNECTION_ID_LENGTH, 6); /* Create DTLS Config */ diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MNetworkConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MNetworkConfig.java index 874bb98eeb..554f13904d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MNetworkConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MNetworkConfig.java @@ -28,7 +28,6 @@ import static org.eclipse.californium.core.config.CoapConfig.DEFAULT_BLOCKWISE_S public class LwM2MNetworkConfig { public static Configuration getCoapConfig(Configuration coapConfig, Integer serverPortNoSec, Integer serverSecurePort, LwM2MTransportServerConfig config) { -// Configuration coapConfig = new Configuration(); coapConfig.set(CoapConfig.COAP_PORT, serverPortNoSec); coapConfig.set(CoapConfig.COAP_SECURE_PORT, serverSecurePort); /** diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java index 064729cbd2..b59914e2ac 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -93,7 +93,7 @@ public class LwM2mServerListener { @Override public void cancelled(Observation observation) { - log.info("Canceled Observation [RegistrationId:{}: {}].", observation.getRegistrationId(), observation instanceof SingleObservation ? + log.trace("Canceled Observation [RegistrationId:{}: {}].", observation.getRegistrationId(), observation instanceof SingleObservation ? "SingleObservation: " + ((SingleObservation) observation).getPath() : "CompositeObservation: " + ((CompositeObservation) observation).getPaths()); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java index 2be7fd5ae1..1e9b8836a9 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java @@ -55,7 +55,7 @@ public class LwM2mSessionMsgListener implements GenericFutureListener1.7.3 2.8.5 - 4.1.0 + 4.2.0 2.7.2 1.5.2 5.9.3 diff --git a/transport/lwm2m/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java b/transport/lwm2m/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java new file mode 100644 index 0000000000..89a2f07179 --- /dev/null +++ b/transport/lwm2m/src/main/java/org/eclipse/leshan/server/observation/ObservationServiceImpl.java @@ -0,0 +1,296 @@ +/** + * Copyright © 2016-2024 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. + * + * Copyright (c) 2016 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + * Michał Wadowski (Orange) - Add Observe-Composite feature. + */ +package org.eclipse.leshan.server.observation; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.peer.LwM2mPeer; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; +import org.eclipse.leshan.core.response.ObserveResponse; +import org.eclipse.leshan.server.endpoint.LwM2mServerEndpoint; +import org.eclipse.leshan.server.endpoint.LwM2mServerEndpointsProvider; +import org.eclipse.leshan.server.profile.ClientProfile; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.registration.RegistrationStore; +import org.eclipse.leshan.server.registration.RegistrationUpdate; +import org.eclipse.leshan.server.registration.UpdatedRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Implementation of the {@link ObservationService} accessing the persisted observation via the provided + * {@link RegistrationStore}. + * + * When a new observation is added or changed or canceled, the registered listeners are notified. + */ +@Slf4j +public class ObservationServiceImpl implements ObservationService, LwM2mNotificationReceiver { + + private final Logger LOG = LoggerFactory.getLogger(ObservationServiceImpl.class); + + private final RegistrationStore registrationStore; + private final LwM2mServerEndpointsProvider endpointProvider; + private final boolean updateRegistrationOnNotification; + + private final List listeners = new CopyOnWriteArrayList<>();; + + /** + * Creates an instance of {@link ObservationServiceImpl} + */ + public ObservationServiceImpl(RegistrationStore store, LwM2mServerEndpointsProvider endpointProvider) { + this(store, endpointProvider, false); + } + + /** + * Creates an instance of {@link ObservationServiceImpl} + * + * @param updateRegistrationOnNotification will activate registration update on observe notification. + * + * @since 1.1 + */ + public ObservationServiceImpl(RegistrationStore store, LwM2mServerEndpointsProvider endpointProvider, + boolean updateRegistrationOnNotification) { + this.registrationStore = store; + this.updateRegistrationOnNotification = updateRegistrationOnNotification; + this.endpointProvider = endpointProvider; + } + + @Override + public int cancelObservations(Registration registration) { + // check registration id + String registrationId = registration.getId(); + if (registrationId == null) + return 0; + + Collection observations = registrationStore.removeObservations(registrationId); + if (observations == null) + return 0; + + for (Observation observation : observations) { + cancel(observation); + } + + return observations.size(); + } + + @Override + public int cancelObservations(Registration registration, String nodePath) { + if (registration == null || registration.getId() == null || nodePath == null || nodePath.isEmpty()) + return 0; + + Set observations = getObservationsForCancel(registration.getId(), nodePath); + for (Observation observation : observations) { + cancelObservation(observation); + } + return observations.size(); + } + + @Override + public int cancelCompositeObservations(Registration registration, String[] nodePaths) { + if (registration == null || registration.getId() == null || nodePaths == null || nodePaths.length == 0) + return 0; + + Set observations = getCompositeObservationsForCancel(registration.getId(), nodePaths); + for (Observation observation : observations) { + cancelObservation(observation); + } + return observations.size(); + } + + @Override + public void cancelObservation(Observation observation) { + if (observation == null) + return; + + registrationStore.removeObservation(observation.getRegistrationId(), observation.getId()); + cancel(observation); + } + + private void cancel(Observation observation) { + List endpoints = endpointProvider.getEndpoints(); + for (LwM2mServerEndpoint lwM2mEndpoint : endpoints) { + lwM2mEndpoint.cancelObservation(observation); + } + + for (ObservationListener listener : listeners) { + listener.cancelled(observation); + } + } + + @Override + public Set getObservations(Registration registration) { + return getObservations(registration.getId()); + } + + private Set getObservations(String registrationId) { + if (registrationId == null) + return Collections.emptySet(); + + return new HashSet<>(registrationStore.getObservations(registrationId)); + } + + private Set getCompositeObservationsForCancel(String registrationId, String[] nodePaths) { + if (registrationId == null || nodePaths == null) + return Collections.emptySet(); + + // array of String to array of LWM2M path + List lwPaths = new ArrayList<>(nodePaths.length); + for (int i = 0; i < nodePaths.length; i++) { + lwPaths.add(new LwM2mPath(nodePaths[i])); + } + + // search composite-observation + Set result = new HashSet<>(); + for (Observation obs : getObservations(registrationId)) { + if (obs instanceof CompositeObservation) { + if (lwPaths.equals(((CompositeObservation) obs).getPaths())) { + result.add(obs); + } + } + } + return result; + } + + private Set getObservationsForCancel(String registrationId, String nodePath) { + if (registrationId == null || nodePath == null) + return Collections.emptySet(); + + Set result = new HashSet<>(); + LwM2mPath lwPath = new LwM2mPath(nodePath); + for (Observation obs : getObservations(registrationId)) { + if (obs instanceof SingleObservation) { + LwM2mPath lwPathObs = ((SingleObservation) obs).getPath(); + if (lwPath.equals(lwPathObs) || lwPathObs.startWith(lwPath)) { // nodePath = "3", lwPathObs = "3/0/9": cancel for tne all lwPathObs + result.add(obs); + } else if (!lwPath.equals(lwPathObs) && lwPath.startWith(lwPathObs)) { // nodePath = "3/0/9", lwPathObs = "3": error... + String errorMsg = String.format( + "Unexpected error : There is registration with id [%s] existing observation [%s] includes input observation [%s]!", + registrationId, lwPathObs, lwPath); + throw new IllegalStateException(errorMsg); + } + } + } + + return result; + } + + @Override + public void addListener(ObservationListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(ObservationListener listener) { + listeners.remove(listener); + } + + private Registration updateRegistrationOnRegistration(Observation observation, LwM2mPeer sender, + ClientProfile profile) { + if (updateRegistrationOnNotification) { + RegistrationUpdate regUpdate = new RegistrationUpdate(observation.getRegistrationId(), sender, null, null, + null, null, null, null, null, null, null, null); + UpdatedRegistration updatedRegistration = registrationStore.updateRegistration(regUpdate); + if (updatedRegistration == null || updatedRegistration.getUpdatedRegistration() == null) { + String errorMsg = String.format( + "Unexpected error: There is no registration with id %s for this observation %s", + observation.getRegistrationId(), observation); + LOG.error(errorMsg); + throw new IllegalStateException(errorMsg); + } + return updatedRegistration.getUpdatedRegistration(); + } + return profile.getRegistration(); + } + + // ********** NotificationListener interface **********// + @Override + public void onNotification(SingleObservation observation, LwM2mPeer sender, ClientProfile profile, + ObserveResponse response) { + try { + Registration updatedRegistration = updateRegistrationOnRegistration(observation, sender, profile); + for (ObservationListener listener : listeners) { + listener.onResponse(observation, updatedRegistration, response); + } + } catch (Exception e) { + for (ObservationListener listener : listeners) { + listener.onError(observation, profile.getRegistration(), e); + } + } + } + + @Override + public void onNotification(CompositeObservation observation, LwM2mPeer sender, ClientProfile profile, + ObserveCompositeResponse response) { + try { + Registration updatedRegistration = updateRegistrationOnRegistration(observation, sender, profile); + for (ObservationListener listener : listeners) { + listener.onResponse(observation, updatedRegistration, response); + } + } catch (Exception e) { + for (ObservationListener listener : listeners) { + listener.onError(observation, profile.getRegistration(), e); + } + } + } + + @Override + public void onError(Observation observation, LwM2mPeer sender, ClientProfile profile, Exception error) { + for (ObservationListener listener : listeners) { + listener.onError(observation, profile.getRegistration(), error); + } + } + + @Override + public void newObservation(Observation observation, Registration registration) { + for (ObservationListener listener : listeners) { + listener.newObservation(observation, registration); + } + } + + @Override + public void cancelled(Observation observation) { + for (ObservationListener listener : listeners) { + listener.cancelled(observation); + } + + } +} From cdc6daa7a906b1d2843a97e923750c6a87778a64 Mon Sep 17 00:00:00 2001 From: nick Date: Mon, 22 Jan 2024 10:57:05 +0200 Subject: [PATCH 18/23] lwm2m: fix buf license excludes --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index eac7903dd7..062a69746c 100755 --- a/pom.xml +++ b/pom.xml @@ -829,6 +829,7 @@ src/main/scripts/control/** src/main/scripts/windows/** src/main/resources/public/static/rulenode/** + src/main/java/org/eclipse/leshan/server/observation//** **/*.proto.js docker/haproxy/** docker/tb-node/** From 41adbe1003f422979b5fa8485d19ab487b27b55c Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 30 Jan 2024 16:37:14 +0200 Subject: [PATCH 19/23] coap: final version --- ...AbstractCoapAttributesIntegrationTest.java | 5 +- .../CoapAttributesUpdatesIntegrationTest.java | 5 ++ ...pAttributesUpdatesJsonIntegrationTest.java | 4 +- ...AttributesUpdatesProtoIntegrationTest.java | 5 +- .../client/CoapClientIntegrationTest.java | 3 ++ ...tractCoapServerSideRpcIntegrationTest.java | 18 +++++-- ...apServerSideRpcDefaultIntegrationTest.java | 3 ++ .../CoapServerSideRpcJsonIntegrationTest.java | 3 ++ ...CoapServerSideRpcProtoIntegrationTest.java | 3 ++ .../transport/coap/CoapTransportResource.java | 40 +++----------- .../coap/callback/CoapResponseCallback.java | 54 +++++++++++++++++++ ...ack.java => CoapResponseCodeCallback.java} | 4 +- .../coap/client/DefaultCoapClientContext.java | 29 +++++++--- pom.xml | 2 +- 14 files changed, 127 insertions(+), 51 deletions(-) create mode 100644 common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapResponseCallback.java rename common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/{CoapOkCallback.java => CoapResponseCodeCallback.java} (88%) diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java index 081e0ce1b2..e8766ea039 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java @@ -25,6 +25,7 @@ import org.awaitility.Awaitility; import org.eclipse.californium.core.CoapObserveRelation; import org.eclipse.californium.core.CoapResponse; import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.CoAP.ResponseCode; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DynamicProtoUtils; @@ -238,7 +239,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap String awaitAlias = "await Json Test Subscribe To AttributesUpdates (client.getObserveRelation)"; await(awaitAlias) .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && + .until(() -> ResponseCode.VALID.equals(callbackCoap.getResponseCode()) && callbackCoap.getObserve() != null && 0 == callbackCoap.getObserve().intValue()); if (emptyCurrentStateNotification) { @@ -285,7 +286,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap); await(awaitAlias) .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && + .until(() -> ResponseCode.VALID.equals(callbackCoap.getResponseCode()) && callbackCoap.getObserve() != null && 0 == callbackCoap.getObserve().intValue()); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java index 9ac2d5fc11..a7de7eb571 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesIntegrationTest.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.server.resources.Resource; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.coapserver.DefaultCoapServerService; @@ -59,11 +60,15 @@ public class CoapAttributesUpdatesIntegrationTest extends AbstractCoapAttributes processAfterTest(); } + + + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { processJsonTestSubscribeToAttributesUpdates(false); } + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception { processJsonTestSubscribeToAttributesUpdates(true); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java index 3fa625796c..4755ba134d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesJsonIntegrationTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.coap.attributes.updates; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.TransportPayloadType; @@ -44,11 +45,12 @@ public class CoapAttributesUpdatesJsonIntegrationTest extends AbstractCoapAttrib processAfterTest(); } + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { processJsonTestSubscribeToAttributesUpdates(false); } - + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception { processJsonTestSubscribeToAttributesUpdates(true); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java index 1ddeabb56d..1c9589bba9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/CoapAttributesUpdatesProtoIntegrationTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.coap.attributes.updates; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.TransportPayloadType; @@ -43,12 +44,12 @@ public class CoapAttributesUpdatesProtoIntegrationTest extends AbstractCoapAttri public void afterTest() throws Exception { processAfterTest(); } - + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { processProtoTestSubscribeToAttributesUpdates(false); } - + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testSubscribeToAttributesUpdatesFromTheServerWithEmptyCurrentStateNotification() throws Exception { processProtoTestSubscribeToAttributesUpdates(true); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java index 5be03683f7..e7a7485ef5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java @@ -26,6 +26,7 @@ import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.DeviceId; @@ -82,6 +83,7 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { processAfterTest(); } + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testConfirmableRequests() throws Exception { boolean confirmable = true; @@ -90,6 +92,7 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { processTestRequestAttributesValuesFromTheServer(confirmable); } + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testNonConfirmableRequests() throws Exception { boolean confirmable = false; diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java index 4a21495a96..384bfe4772 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java @@ -82,15 +82,27 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC .until(() -> CoAP.ResponseCode.VALID.equals(callbackCoap.getResponseCode()) && callbackCoap.getObserve() != null && 0 == callbackCoap.getObserve()); validateCurrentStateNotification(callbackCoap); - int expectedObserveAfterRpcProcessed = callbackCoap.getObserve() + 1; - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}"; + + int expectedObserveAfterRpcProcessed1 = callbackCoap.getObserve() + 1; + String setGpioRequest = "{\"method\":\"setGpio1\",\"params\":{\"pin\": \"21\",\"value\": 1}}"; String deviceId = savedDevice.getId().getId().toString(); String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk()); awaitAlias = "await One Way Rpc setGpio(method, params, value)"; await(awaitAlias) .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) .until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && - callbackCoap.getObserve() != null && expectedObserveAfterRpcProcessed == callbackCoap.getObserve()); + callbackCoap.getObserve() != null && expectedObserveAfterRpcProcessed1 == callbackCoap.getObserve()); + validateOneWayStateChangedNotification(callbackCoap, result); + + int expectedObserveAfterRpcProcessed2 = callbackCoap.getObserve() + 1; + setGpioRequest = "{\"method\":\"setGpio2\",\"params\":{\"pin\": \"22\",\"value\": 2}}"; + deviceId = savedDevice.getId().getId().toString(); + result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk()); + awaitAlias = "await One Way Rpc setGpio(method, params, value)"; + await(awaitAlias) + .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && + callbackCoap.getObserve() != null && expectedObserveAfterRpcProcessed2 == callbackCoap.getObserve()); validateOneWayStateChangedNotification(callbackCoap, result); observeRelation.proactiveCancel(); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java index df1159b128..8f9d3379ef 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcDefaultIntegrationTest.java @@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.security.AccessValidator; @@ -82,11 +83,13 @@ public class CoapServerSideRpcDefaultIntegrationTest extends AbstractCoapServerS Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result); } + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(false); } + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest("{\"value1\":\"A\",\"value2\":\"B\"}", false); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java index 5c94e91b72..34d678b5b5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcJsonIntegrationTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.coap.rpc; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.TransportPayloadType; @@ -43,11 +44,13 @@ public class CoapServerSideRpcJsonIntegrationTest extends AbstractCoapServerSide processAfterTest(); } + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(false); } + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest("{\"value1\":\"A\",\"value2\":\"B\"}", false); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java index 2f7d46a390..138f87e964 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/CoapServerSideRpcProtoIntegrationTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.coap.rpc; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.TransportPayloadType; @@ -44,11 +45,13 @@ public class CoapServerSideRpcProtoIntegrationTest extends AbstractCoapServerSid processAfterTest(); } + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(true); } + @Ignore // Uncomment when Californium 3.11 is released with https://github.com/eclipse-californium/californium/pull/2215 @Test public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest("{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}", true); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index 2528c0992e..ebc52fc034 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -19,7 +19,6 @@ import com.google.gson.JsonParseException; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; -import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.network.Exchange; import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.server.resources.CoapExchange; @@ -27,6 +26,8 @@ import org.eclipse.californium.core.server.resources.Resource; import org.eclipse.californium.core.server.resources.ResourceObserver; import org.thingsboard.server.coapserver.CoapServerService; import org.thingsboard.server.coapserver.TbCoapDtlsSessionInfo; +import org.thingsboard.server.common.adaptor.AdaptorException; +import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; @@ -35,13 +36,11 @@ import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.security.DeviceTokenCredentials; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.transport.TransportServiceCallback; -import org.thingsboard.server.common.adaptor.AdaptorException; -import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback; import org.thingsboard.server.transport.coap.callback.CoapNoOpCallback; -import org.thingsboard.server.transport.coap.callback.CoapOkCallback; +import org.thingsboard.server.transport.coap.callback.CoapResponseCodeCallback; import org.thingsboard.server.transport.coap.callback.GetAttributesSyncSessionCallback; import org.thingsboard.server.transport.coap.callback.ToServerRpcSyncSessionCallback; import org.thingsboard.server.transport.coap.client.CoapClientContext; @@ -54,7 +53,6 @@ import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import static org.eclipse.californium.elements.DtlsEndpointContext.KEY_SESSION_ID; @@ -84,30 +82,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource { ctx.getScheduler().scheduleAtFixedRate(clients::reportActivity, new Random().nextInt((int) sessionReportTimeout), sessionReportTimeout, TimeUnit.MILLISECONDS); } - /* - * Overwritten method from CoapResource to be able to manage our own observe notification counters. - */ - @Override - public void checkObserveRelation(Exchange exchange, Response response) { - String token = getTokenFromRequest(exchange.getRequest()); - final ObserveRelation relation = exchange.getRelation(); - if (relation == null || relation.isCanceled()) { - return; // because request did not try to establish a relation - } - if (response.getCode().isSuccess()) { - if (!relation.isEstablished()) { - relation.setEstablished(); - addObserveRelation(relation); - } - AtomicInteger state = clients.getNotificationCounterByToken(token); - if (state != null) { - response.getOptions().setObserve(state.getAndIncrement()); - } else { - response.getOptions().removeObserve(); - } - } // ObserveLayer takes care of the else case - } - @Override protected void processHandleGet(CoapExchange exchange) { Optional featureType = getFeatureType(exchange.advanced().getRequest()); @@ -278,7 +252,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { UUID sessionId = toSessionId(sessionInfo); transportService.process(sessionInfo, clientState.getAdaptor().convertToPostAttributes(sessionId, request, clientState.getConfiguration().getAttributesMsgDescriptor()), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); } private void handlePostTelemetryRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException { @@ -286,7 +260,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { UUID sessionId = toSessionId(sessionInfo); transportService.process(sessionInfo, clientState.getAdaptor().convertToPostTelemetry(sessionId, request, clientState.getConfiguration().getTelemetryMsgDescriptor()), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); } private void handleClaimRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) throws AdaptorException { @@ -294,7 +268,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { UUID sessionId = toSessionId(sessionInfo); transportService.process(sessionInfo, clientState.getAdaptor().convertToClaimDevice(sessionId, request, sessionInfo), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); } private void handleAttributeSubscribeRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) { @@ -320,7 +294,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { UUID sessionId = toSessionId(session); transportService.process(session, clientState.getAdaptor().convertToDeviceRpcResponse(sessionId, request, clientState.getConfiguration().getRpcResponseMsgDescriptor()), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); } private void handleRpcSubscribeRequest(TbCoapClientState clientState, CoapExchange exchange, Request request) { diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapResponseCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapResponseCallback.java new file mode 100644 index 0000000000..c0baf62bc1 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapResponseCallback.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2024 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.coap.callback; + +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.thingsboard.server.common.transport.TransportServiceCallback; + +public class CoapResponseCallback implements TransportServiceCallback { + + protected final CoapExchange exchange; + protected final Response onSuccessResponse; + protected final Response onFailureResponse; + + public CoapResponseCallback(CoapExchange exchange, Response onSuccessResponse, Response onFailureResponse) { + this.exchange = exchange; + this.onSuccessResponse = onSuccessResponse; + this.onFailureResponse = onFailureResponse; + } + + /** + * @param msg + */ + @Override + public void onSuccess(Void msg) { + this.onSuccessResponse.setConfirmable(isConRequest()); + exchange.respond(this.onSuccessResponse); + } + + /** + * @param e + */ + @Override + public void onError(Throwable e) { + exchange.respond(onFailureResponse); + } + + protected boolean isConRequest() { + return exchange.advanced().getRequest().isConfirmable(); + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapResponseCodeCallback.java similarity index 88% rename from common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java rename to common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapResponseCodeCallback.java index a45821d9d6..21293395ea 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapResponseCodeCallback.java @@ -20,13 +20,13 @@ import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.server.resources.CoapExchange; import org.thingsboard.server.common.transport.TransportServiceCallback; -public class CoapOkCallback implements TransportServiceCallback { +public class CoapResponseCodeCallback implements TransportServiceCallback { protected final CoapExchange exchange; protected final CoAP.ResponseCode onSuccessResponse; protected final CoAP.ResponseCode onFailureResponse; - public CoapOkCallback(CoapExchange exchange, CoAP.ResponseCode onSuccessResponse, CoAP.ResponseCode onFailureResponse) { + public CoapResponseCodeCallback(CoapExchange exchange, CoAP.ResponseCode onSuccessResponse, CoAP.ResponseCode onFailureResponse) { this.exchange = exchange; this.onSuccessResponse = onSuccessResponse; this.onFailureResponse = onFailureResponse; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java index 735ec1d524..2140e1966c 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.coap.client; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.CoAP.ResponseCode; import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.server.resources.CoapExchange; @@ -26,6 +27,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.server.coapserver.CoapServerContext; +import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -45,7 +47,6 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.msg.session.FeatureType; -import org.thingsboard.server.transport.coap.CoapSessionMsgType; import org.thingsboard.server.common.transport.DeviceDeletedEvent; import org.thingsboard.server.common.transport.DeviceProfileUpdatedEvent; import org.thingsboard.server.common.transport.DeviceUpdatedEvent; @@ -53,18 +54,19 @@ import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportDeviceProfileCache; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; -import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.transport.coap.CoapSessionMsgType; import org.thingsboard.server.transport.coap.CoapTransportContext; import org.thingsboard.server.transport.coap.TbCoapMessageObserver; import org.thingsboard.server.transport.coap.TransportConfigurationContainer; import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor; import org.thingsboard.server.transport.coap.callback.AbstractSyncSessionCallback; import org.thingsboard.server.transport.coap.callback.CoapNoOpCallback; -import org.thingsboard.server.transport.coap.callback.CoapOkCallback; +import org.thingsboard.server.transport.coap.callback.CoapResponseCallback; +import org.thingsboard.server.transport.coap.callback.CoapResponseCodeCallback; import java.util.Optional; import java.util.UUID; @@ -329,9 +331,14 @@ public class DefaultCoapClientContext implements CoapClientContext { TransportProtos.GetAttributeRequestMsg.newBuilder().setOnlyShared(true).build(), new CoapNoOpCallback(exchange)); } else { + Response response = new Response(CoAP.ResponseCode.VALID); + if (state.getRpc() == null) { + state.setRpc(new TbCoapObservationState(exchange, token)); + } + response.getOptions().setObserve(state.getRpc().getObserveCounter().getAndIncrement()); transportService.process(state.getSession(), TransportProtos.SubscribeToRPCMsg.getDefaultInstance(), - new CoapOkCallback(exchange, CoAP.ResponseCode.VALID, CoAP.ResponseCode.INTERNAL_SERVER_ERROR) + new CoapResponseCallback(exchange, response, new Response(CoAP.ResponseCode.INTERNAL_SERVER_ERROR)) ); } } @@ -478,7 +485,13 @@ public class DefaultCoapClientContext implements CoapClientContext { TbCoapObservationState attrs = state.getAttrs(); if (attrs != null) { try { - Response response = state.getAdaptor().convertToPublish(msg); + Response resp = state.getAdaptor().convertToPublish(msg); + Response response = new Response(ResponseCode.VALID); + response.setPayload(resp.getPayload()); + if (state.getRpc() == null) { + state.setRpc(new TbCoapObservationState(attrs.getExchange(), attrs.getToken())); + } + response.getOptions().setObserve(state.getRpc().getObserveCounter().getAndIncrement()); respond(attrs.getExchange(), response, state.getContentFormat()); } catch (AdaptorException e) { log.trace("Failed to reply due to error", e); @@ -509,6 +522,7 @@ public class DefaultCoapClientContext implements CoapClientContext { boolean conRequest = AbstractSyncSessionCallback.isConRequest(state.getAttrs()); int requestId = getNextMsgId(); Response response = state.getAdaptor().convertToPublish(msg); + response.getOptions().setObserve(state.getRpc().getObserveCounter().getAndIncrement()); response.setConfirmable(conRequest); response.setMID(requestId); if (conRequest) { @@ -573,6 +587,7 @@ public class DefaultCoapClientContext implements CoapClientContext { int requestId = getNextMsgId(); try { Response response = state.getAdaptor().convertToPublish(msg, state.getConfiguration().getRpcRequestDynamicMessageBuilder()); + response.getOptions().setObserve(state.getRpc().getObserveCounter().getAndIncrement()); response.setConfirmable(conRequest); response.setMID(requestId); if (conRequest) { @@ -808,7 +823,7 @@ public class DefaultCoapClientContext implements CoapClientContext { state.setRpc(null); transportService.process(state.getSession(), TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), - new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); if (state.getAttrs() == null) { closeAndCleanup(state); } @@ -822,7 +837,7 @@ public class DefaultCoapClientContext implements CoapClientContext { state.setAttrs(null); transportService.process(state.getSession(), TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), - new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + new CoapResponseCodeCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); if (state.getRpc() == null) { closeAndCleanup(state); } diff --git a/pom.xml b/pom.xml index 062a69746c..9079dc08e9 100755 --- a/pom.xml +++ b/pom.xml @@ -72,7 +72,7 @@ 1.3.4 4.2.1 2.2.6 - 3.9.1 + 3.10.0 2.0.0-M14 2.9.0 2.3.30 From 4eda0d668c36cf763538a11958fe198ac023a92f Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 30 Jan 2024 18:49:21 +0200 Subject: [PATCH 20/23] coap: msa tests --- .../server/msa/TestCoapClient.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClient.java index 67638ebefb..8fa6c44f4a 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClient.java @@ -22,6 +22,11 @@ import org.eclipse.californium.core.CoapResponse; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.elements.config.Configuration.ModuleDefinitionsProvider; +import org.eclipse.californium.elements.config.IntegerDefinition; +import org.eclipse.californium.elements.config.StringDefinition; +import org.eclipse.californium.elements.config.TcpConfig; import org.eclipse.californium.elements.exception.ConnectorException; import org.thingsboard.server.common.msg.session.FeatureType; @@ -32,6 +37,26 @@ public class TestCoapClient { private static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/"; private static final long CLIENT_REQUEST_TIMEOUT = 60000L; + + private static final String MODULE2 = "TEST2."; + private static final IntegerDefinition INT2 = new IntegerDefinition(MODULE2 + "INT2", "TEST", null, 1); + private static final StringDefinition STRING = new StringDefinition(MODULE2 + "STRING", "TEST"); + + private static final ModuleDefinitionsProvider DEFAULTS = new ModuleDefinitionsProvider() { + + @Override + public String getModule() { + return MODULE2; + } + + @Override + public void applyDefinitions(Configuration config) { + TcpConfig.register(); + config.set(INT2, 100); + config.set(STRING, "Hallo"); + } + }; + private final CoapClient client; public TestCoapClient(){ @@ -43,7 +68,8 @@ public class TestCoapClient { } public TestCoapClient(String featureTokenUrl) { - this.client = createClient(featureTokenUrl); + Configuration.addDefaultModule(DEFAULTS); + this.client = new CoapClient(featureTokenUrl); } public void connectToCoap(String accessToken) { From 537a8be9828b2a6edc915f77d37d3b3e6941497e Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 31 Jan 2024 13:32:47 +0200 Subject: [PATCH 21/23] coap: comments1 --- .../transport/coap/client/DefaultCoapClientContext.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java index 2140e1966c..08e4c513df 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java @@ -488,10 +488,7 @@ public class DefaultCoapClientContext implements CoapClientContext { Response resp = state.getAdaptor().convertToPublish(msg); Response response = new Response(ResponseCode.VALID); response.setPayload(resp.getPayload()); - if (state.getRpc() == null) { - state.setRpc(new TbCoapObservationState(attrs.getExchange(), attrs.getToken())); - } - response.getOptions().setObserve(state.getRpc().getObserveCounter().getAndIncrement()); + response.getOptions().setObserve(attrs.getObserveCounter().getAndIncrement()); respond(attrs.getExchange(), response, state.getContentFormat()); } catch (AdaptorException e) { log.trace("Failed to reply due to error", e); @@ -522,7 +519,7 @@ public class DefaultCoapClientContext implements CoapClientContext { boolean conRequest = AbstractSyncSessionCallback.isConRequest(state.getAttrs()); int requestId = getNextMsgId(); Response response = state.getAdaptor().convertToPublish(msg); - response.getOptions().setObserve(state.getRpc().getObserveCounter().getAndIncrement()); + response.getOptions().setObserve(attrs.getObserveCounter().getAndIncrement()); response.setConfirmable(conRequest); response.setMID(requestId); if (conRequest) { From 7748c56bc3e5d39a9e5d1f5b194d5ce57441d81f Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 1 Feb 2024 15:47:26 +0200 Subject: [PATCH 22/23] coap: comments1 --- .../server/msa/AbstractCoapClientTest.java | 81 ++++++++++ .../server/msa/TestCoapClient.java | 148 ------------------ .../server/msa/TestCoapClientCallback.java | 68 -------- .../msa/connectivity/CoapClientTest.java | 28 +--- 4 files changed, 85 insertions(+), 240 deletions(-) create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractCoapClientTest.java delete mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClient.java delete mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClientCallback.java diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractCoapClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractCoapClientTest.java new file mode 100644 index 0000000000..4ed5426bc4 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractCoapClientTest.java @@ -0,0 +1,81 @@ +/** + * Copyright © 2016-2024 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.msa; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.config.CoapConfig; +import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.elements.config.Configuration.ModuleDefinitionsProvider; +import org.eclipse.californium.elements.config.IntegerDefinition; +import org.eclipse.californium.elements.config.TcpConfig; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.msg.session.FeatureType; + +public abstract class AbstractCoapClientTest extends AbstractContainerTest{ + + private static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/"; + private static final long CLIENT_REQUEST_TIMEOUT = 60000L; + + + private static final String COAP_CLIENT_TEST = "COAP_CLIENT_TEST."; + private static final IntegerDefinition COAP_PORT_DEF = CoapConfig.COAP_PORT; + + private static final ModuleDefinitionsProvider MODULE_DEFINITIONS_PROVIDER = new ModuleDefinitionsProvider() { + + @Override + public String getModule() { + return COAP_CLIENT_TEST; + } + + @Override + public void applyDefinitions(Configuration config) { + TcpConfig.register(); + config.set(COAP_PORT_DEF, 5683); + } + }; + + protected CoapClient client; + + protected byte[] createCoapClientAndPublish(String deviceName) throws Exception { + String provisionRequestMsg = createTestProvisionMessage(deviceName); + Configuration.addDefaultModule(MODULE_DEFINITIONS_PROVIDER); + String featureTokenUrl = COAP_BASE_URL + FeatureType.PROVISION.name().toLowerCase(); + client = new CoapClient(featureTokenUrl); + return client.setTimeout(CLIENT_REQUEST_TIMEOUT) + .post(provisionRequestMsg.getBytes(), MediaTypeRegistry.APPLICATION_JSON) + .getPayload(); + } + + protected void disconnect() { + if (client != null) { + client.shutdown(); + } + } + + private String createTestProvisionMessage(String deviceName) { + ObjectNode provisionRequest = JacksonUtil.newObjectNode(); + provisionRequest.put("provisionDeviceKey", TEST_PROVISION_DEVICE_KEY); + provisionRequest.put("provisionDeviceSecret", TEST_PROVISION_DEVICE_SECRET); + if (deviceName != null) { + provisionRequest.put("deviceName", deviceName); + } + return provisionRequest.toString(); + } +} + + diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClient.java deleted file mode 100644 index 8fa6c44f4a..0000000000 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClient.java +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Copyright © 2016-2024 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.msa; - -import org.eclipse.californium.core.CoapClient; -import org.eclipse.californium.core.CoapHandler; -import org.eclipse.californium.core.CoapObserveRelation; -import org.eclipse.californium.core.CoapResponse; -import org.eclipse.californium.core.coap.CoAP; -import org.eclipse.californium.core.coap.MediaTypeRegistry; -import org.eclipse.californium.core.coap.Request; -import org.eclipse.californium.elements.config.Configuration; -import org.eclipse.californium.elements.config.Configuration.ModuleDefinitionsProvider; -import org.eclipse.californium.elements.config.IntegerDefinition; -import org.eclipse.californium.elements.config.StringDefinition; -import org.eclipse.californium.elements.config.TcpConfig; -import org.eclipse.californium.elements.exception.ConnectorException; -import org.thingsboard.server.common.msg.session.FeatureType; - -import java.io.IOException; - -public class TestCoapClient { - - private static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/"; - private static final long CLIENT_REQUEST_TIMEOUT = 60000L; - - - private static final String MODULE2 = "TEST2."; - private static final IntegerDefinition INT2 = new IntegerDefinition(MODULE2 + "INT2", "TEST", null, 1); - private static final StringDefinition STRING = new StringDefinition(MODULE2 + "STRING", "TEST"); - - private static final ModuleDefinitionsProvider DEFAULTS = new ModuleDefinitionsProvider() { - - @Override - public String getModule() { - return MODULE2; - } - - @Override - public void applyDefinitions(Configuration config) { - TcpConfig.register(); - config.set(INT2, 100); - config.set(STRING, "Hallo"); - } - }; - - private final CoapClient client; - - public TestCoapClient(){ - this.client = createClient(); - } - - public TestCoapClient(String accessToken, FeatureType featureType) { - this.client = createClient(getFeatureTokenUrl(accessToken, featureType)); - } - - public TestCoapClient(String featureTokenUrl) { - Configuration.addDefaultModule(DEFAULTS); - this.client = new CoapClient(featureTokenUrl); - } - - public void connectToCoap(String accessToken) { - setURI(accessToken, null); - } - - public void connectToCoap(String accessToken, FeatureType featureType) { - setURI(accessToken, featureType); - } - - public void disconnect() { - if (client != null) { - client.shutdown(); - } - } - - public CoapResponse postMethod(String requestBody) throws ConnectorException, IOException { - return this.postMethod(requestBody.getBytes()); - } - - public CoapResponse postMethod(byte[] requestBodyBytes) throws ConnectorException, IOException { - return client.setTimeout(CLIENT_REQUEST_TIMEOUT).post(requestBodyBytes, MediaTypeRegistry.APPLICATION_JSON); - } - - public void postMethod(CoapHandler handler, String payload, int format) { - client.post(handler, payload, format); - } - - public void postMethod(CoapHandler handler, byte[] payload, int format) { - client.post(handler, payload, format); - } - - public CoapResponse getMethod() throws ConnectorException, IOException { - return client.setTimeout(CLIENT_REQUEST_TIMEOUT).get(); - } - - public CoapObserveRelation getObserveRelation(TestCoapClientCallback callback){ - Request request = Request.newGet().setObserve(); - request.setType(CoAP.Type.CON); - return client.observe(request, callback); - } - - public void setURI(String featureTokenUrl) { - if (client == null) { - throw new RuntimeException("Failed to connect! CoapClient is not initialized!"); - } - client.setURI(featureTokenUrl); - } - - public void setURI(String accessToken, FeatureType featureType) { - if (featureType == null){ - featureType = FeatureType.ATTRIBUTES; - } - setURI(getFeatureTokenUrl(accessToken, featureType)); - } - - private CoapClient createClient() { - return new CoapClient(); - } - - private CoapClient createClient(String featureTokenUrl) { - return new CoapClient(featureTokenUrl); - } - - public static String getFeatureTokenUrl(FeatureType featureType) { - return COAP_BASE_URL + featureType.name().toLowerCase(); - } - - public static String getFeatureTokenUrl(String token, FeatureType featureType) { - return COAP_BASE_URL + token + "/" + featureType.name().toLowerCase(); - } - - public static String getFeatureTokenUrl(String token, FeatureType featureType, int requestId) { - return COAP_BASE_URL + token + "/" + featureType.name().toLowerCase() + "/" + requestId; - } -} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClientCallback.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClientCallback.java deleted file mode 100644 index 49c5a06476..0000000000 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestCoapClientCallback.java +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright © 2016-2024 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.msa; - -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.CoapHandler; -import org.eclipse.californium.core.CoapResponse; -import org.eclipse.californium.core.coap.CoAP; - -import java.util.concurrent.CountDownLatch; - -@Slf4j -@Data -public class TestCoapClientCallback implements CoapHandler { - - protected final CountDownLatch latch; - protected Integer observe; - protected byte[] payloadBytes; - protected CoAP.ResponseCode responseCode; - - public TestCoapClientCallback() { - this.latch = new CountDownLatch(1); - } - - public TestCoapClientCallback(int subscribeCount) { - this.latch = new CountDownLatch(subscribeCount); - } - - public Integer getObserve() { - return observe; - } - - public byte[] getPayloadBytes() { - return payloadBytes; - } - - public CoAP.ResponseCode getResponseCode() { - return responseCode; - } - - @Override - public void onLoad(CoapResponse response) { - observe = response.getOptions().getObserve(); - payloadBytes = response.getPayload(); - responseCode = response.getCode(); - latch.countDown(); - } - - @Override - public void onError() { - log.warn("Command Response Ack Error, No connect"); - } - -} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java index 4dc7d91b2a..a65aff1bc1 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.msa.connectivity; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.gson.JsonObject; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; @@ -26,18 +25,14 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.common.msg.session.FeatureType; -import org.thingsboard.server.msa.AbstractContainerTest; +import org.thingsboard.server.msa.AbstractCoapClientTest; import org.thingsboard.server.msa.DisableUIListeners; -import org.thingsboard.server.msa.TestCoapClient; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; @DisableUIListeners -public class CoapClientTest extends AbstractContainerTest { - private TestCoapClient client; - +public class CoapClientTest extends AbstractCoapClientTest{ private Device device; @BeforeMethod public void setUp() throws Exception { @@ -48,6 +43,7 @@ public class CoapClientTest extends AbstractContainerTest { @AfterMethod public void tearDown() { testRestClient.deleteDeviceIfExists(device.getId()); + disconnect(); } @Test @@ -101,21 +97,5 @@ public class CoapClientTest extends AbstractContainerTest { assertThat(response.get("status").asText()).isEqualTo("NOT_FOUND"); } - - private byte[] createCoapClientAndPublish(String deviceName) throws Exception { - String provisionRequestMsg = createTestProvisionMessage(deviceName); - client = new TestCoapClient(TestCoapClient.getFeatureTokenUrl(FeatureType.PROVISION)); - return client.postMethod(provisionRequestMsg.getBytes()).getPayload(); - } - - private String createTestProvisionMessage(String deviceName) { - ObjectNode provisionRequest = JacksonUtil.newObjectNode(); - provisionRequest.put("provisionDeviceKey", TEST_PROVISION_DEVICE_KEY); - provisionRequest.put("provisionDeviceSecret", TEST_PROVISION_DEVICE_SECRET); - if (deviceName != null) { - provisionRequest.put("deviceName", deviceName); - } - return provisionRequest.toString(); - } - } + From a89802e801a73245daa38b4f831dd863b6197794 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 2 Feb 2024 17:23:11 +0200 Subject: [PATCH 23/23] Response code fix --- .../attributes/AbstractCoapAttributesIntegrationTest.java | 4 ++-- .../transport/coap/client/DefaultCoapClientContext.java | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java index e8766ea039..92554b3067 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java @@ -239,7 +239,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap String awaitAlias = "await Json Test Subscribe To AttributesUpdates (client.getObserveRelation)"; await(awaitAlias) .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .until(() -> ResponseCode.VALID.equals(callbackCoap.getResponseCode()) && + .until(() -> ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && callbackCoap.getObserve() != null && 0 == callbackCoap.getObserve().intValue()); if (emptyCurrentStateNotification) { @@ -286,7 +286,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap); await(awaitAlias) .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .until(() -> ResponseCode.VALID.equals(callbackCoap.getResponseCode()) && + .until(() -> ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && callbackCoap.getObserve() != null && 0 == callbackCoap.getObserve().intValue()); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java index 08e4c513df..7177542c3d 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java @@ -485,9 +485,7 @@ public class DefaultCoapClientContext implements CoapClientContext { TbCoapObservationState attrs = state.getAttrs(); if (attrs != null) { try { - Response resp = state.getAdaptor().convertToPublish(msg); - Response response = new Response(ResponseCode.VALID); - response.setPayload(resp.getPayload()); + Response response = state.getAdaptor().convertToPublish(msg); response.getOptions().setObserve(attrs.getObserveCounter().getAndIncrement()); respond(attrs.getExchange(), response, state.getContentFormat()); } catch (AdaptorException e) {