Repository: eclipse/leshan
Pull request #1030 (2021-06-18 13:14:24)

#1024 :  fix issue with long resource id in LwM2mNodeSenMLEncoder
This bug affected SenML CBOR and SenML JSON for multi instance resource
where resource id and or resource instance id is pretty long.

This PR aims to fix that (see #1024)


Patch:
diff --git a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java
index cd051f5b..f395d36e 100644
--- a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java
+++ b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java
@@ -31,6 +31,7 @@ import java.security.cert.X509Certificate;
 import java.security.interfaces.ECPublicKey;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashMap;
@@ -76,6 +77,9 @@ import org.eclipse.leshan.core.californium.DefaultEndpointFactory;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectLoader;
 import org.eclipse.leshan.core.model.ObjectModel;
+import org.eclipse.leshan.core.model.ResourceModel;
+import org.eclipse.leshan.core.model.ResourceModel.Operations;
+import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
 import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
@@ -645,6 +649,12 @@ public class LeshanClientDemo {
             models.addAll(ObjectLoader.loadObjectsFromDir(new File(modelsFolderPath), true));
         }
 
+        Collection<ResourceModel> resources = Arrays
+                .asList(new ResourceModel(1002, "string", Operations.RW, true, false, Type.STRING, null, null, null));
+        ObjectModel objectModel = new ObjectModel(666, "test", "description", "1.0", false, false, resources, null,
+                null, "description");
+        models.add(objectModel);
+
         // Initialize object list
         final LwM2mModel model = new StaticModel(models);
         final ObjectsInitializer initializer = new ObjectsInitializer(model);
@@ -684,6 +694,7 @@ public class LeshanClientDemo {
         initializer.setInstancesForObject(DEVICE, new MyDevice());
         initializer.setInstancesForObject(LOCATION, locationInstance);
         initializer.setInstancesForObject(OBJECT_ID_TEMPERATURE_SENSOR, new RandomTemperatureSensor());
+        initializer.setInstancesForObject(666, new TestData());
         List<LwM2mObjectEnabler> enablers = initializer.createAll();
 
         // Create CoAP Config
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java
index 864320b7..0042cc73 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java
@@ -250,10 +250,13 @@ public class LwM2mNodeSenMLEncoder implements TimestampedNodeEncoder, MultiNodeE
             String bn = requestPath.toString();
             String n = recordName == null ? "" : recordName;
 
+            // Add slash if necessary
+            if (!n.isEmpty()) {
+                bn += "/";
+            }
+
+            // Set basename only for first record
             if (records.isEmpty()) {
-                if (!n.isEmpty()) {
-                    bn += "/";
-                }
                 record.setBaseName(bn);
             }
             record.setName(n);


Test patch:
diff --git a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/TestData.java b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/TestData.java
new file mode 100644
index 00000000..4812229e
--- /dev/null
+++ b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/TestData.java
@@ -0,0 +1,262 @@
+package org.eclipse.leshan.client.demo;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+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.node.LwM2mResourceInstance;
+import org.eclipse.leshan.core.node.ObjectLink;
+import org.eclipse.leshan.core.response.ReadResponse;
+import org.eclipse.leshan.core.response.WriteResponse;
+import org.eclipse.leshan.core.util.NamedThreadFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TestData extends BaseInstanceEnabler implements Destroyable {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TestData.class);
+
+    private static final int INT_VAL = 100;
+    private static final int FLOAT_VAL = 101;
+    private static final int STRING_VAL = 102;
+    private static final int BOOL_VAL = 103;
+    private static final int TIME_VAL = 104;
+    private static final int LINK_VAL = 105;
+    private static final int OPAQUE_VAL = 106;
+    private static final int INT_ARRAY_VAL = 1000;
+    private static final int FLOAT_ARRAY_VAL = 1001;
+    private static final int STRING_ARRAY_VAL = 1002;
+    private static final int BOOL_ARRAY_VAL = 1003;
+    private static final int TIME_ARRAY_VAL = 1004;
+    private static final int LINK_ARRAY_VAL = 1005;
+    private static final int OPAQUE_ARRAY_VAL = 1006;
+    private static final List<Integer> supportedResources = Arrays.asList(INT_VAL, FLOAT_VAL, STRING_VAL, BOOL_VAL,
+            TIME_VAL, LINK_VAL, OPAQUE_VAL, INT_ARRAY_VAL, FLOAT_ARRAY_VAL, STRING_ARRAY_VAL, BOOL_ARRAY_VAL,
+            TIME_ARRAY_VAL, LINK_ARRAY_VAL, OPAQUE_ARRAY_VAL);
+    private final ScheduledExecutorService scheduler;
+    private int intVal = 20;
+    private double floatVal = 20d;
+    private String stringVal = "hello world";
+    private boolean boolVal = true;
+    private Date timeVal = new Date();
+    private ObjectLink linkVal = new ObjectLink(3, 0);
+    private byte[] opaqueVal = fromHexString("00112233445566778899");
+    private Map<Integer, Long> intArrayVal = new HashMap<>();
+    private Map<Integer, Double> floatArrayVal = new HashMap<>();
+    private Map<Integer, String> stringArrayVal = new HashMap<>();
+    private Map<Integer, Boolean> boolArrayVal = new HashMap<>();
+    private Map<Integer, Date> timeArrayVal = new HashMap<>();
+    private Map<Integer, ObjectLink> linkArrayVal = new HashMap<>();
+    private Map<Integer, byte[]> opaqueArrayVal = new HashMap<>();
+
+    private static byte[] fromHexString(String src) {
+        byte[] biBytes = new BigInteger("10" + src.replaceAll("\\s", ""), 16).toByteArray();
+        return Arrays.copyOfRange(biBytes, 1, biBytes.length);
+    }
+
+    public TestData() {
+        intArrayVal.put(0, 10L);
+        intArrayVal.put(2, 20L);
+        intArrayVal.put(3, 30L);
+        floatArrayVal.put(0, 1.5d);
+        floatArrayVal.put(1, 2.5d);
+        stringArrayVal.put(10, "hello world");
+        stringArrayVal.put(20, "another test");
+        boolArrayVal.put(0, false);
+        boolArrayVal.put(1, true);
+        timeArrayVal.put(0, new Date());
+        timeArrayVal.put(1, new Date());
+        linkArrayVal.put(10, new ObjectLink(3, 0));
+        linkArrayVal.put(11, new ObjectLink(7, 0));
+        opaqueArrayVal.put(12, fromHexString("AABBCCDD"));
+        opaqueArrayVal.put(24, fromHexString("11223344"));
+        this.scheduler = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("Test Incrementer"));
+        scheduler.scheduleAtFixedRate(new Runnable() {
+
+            @Override
+            public void run() {
+                adjustValues();
+            }
+        }, 2, 2, TimeUnit.SECONDS);
+    }
+
+    @Override
+    public synchronized ReadResponse read(ServerIdentity identity, int resourceId) {
+        LOG.info("Read on test resource /{}/{}/{}", getModel().id, getId(), resourceId);
+        switch (resourceId) {
+        case INT_VAL:
+            return ReadResponse.success(resourceId, intVal);
+        case FLOAT_VAL:
+            return ReadResponse.success(resourceId, floatVal);
+        case STRING_VAL:
+            return ReadResponse.success(resourceId, stringVal);
+        case BOOL_VAL:
+            return ReadResponse.success(resourceId, boolVal);
+        case TIME_VAL:
+            return ReadResponse.success(resourceId, timeVal);
+        case LINK_VAL:
+            return ReadResponse.success(resourceId, linkVal);
+        case OPAQUE_VAL:
+            return ReadResponse.success(resourceId, opaqueVal);
+        case INT_ARRAY_VAL:
+            return ReadResponse.success(resourceId, intArrayVal, ResourceModel.Type.INTEGER);
+        case FLOAT_ARRAY_VAL:
+            return ReadResponse.success(resourceId, floatArrayVal, ResourceModel.Type.FLOAT);
+        case STRING_ARRAY_VAL:
+            return ReadResponse.success(resourceId, stringArrayVal, ResourceModel.Type.STRING);
+        case BOOL_ARRAY_VAL:
+            return ReadResponse.success(resourceId, boolArrayVal, ResourceModel.Type.BOOLEAN);
+        case TIME_ARRAY_VAL:
+            return ReadResponse.success(resourceId, timeArrayVal, ResourceModel.Type.TIME);
+        case LINK_ARRAY_VAL:
+            return ReadResponse.success(resourceId, linkArrayVal, ResourceModel.Type.OBJLNK);
+        case OPAQUE_ARRAY_VAL:
+            return ReadResponse.success(resourceId, opaqueArrayVal, ResourceModel.Type.OPAQUE);
+        default:
+            return super.read(identity, resourceId);
+        }
+    }
+
+    @Override
+    public synchronized WriteResponse write(ServerIdentity identity, boolean replace, int resourceId,
+            LwM2mResource value) {
+        LOG.info("Write on test resource /{}/{}/{}", getModel().id, getId(), resourceId);
+        switch (resourceId) {
+        case INT_VAL:
+            intVal = ((Long) value.getValue()).intValue();
+            return WriteResponse.success();
+        case FLOAT_VAL:
+            floatVal = (double) value.getValue();
+            return WriteResponse.success();
+        case STRING_VAL:
+            stringVal = (String) value.getValue();
+            return WriteResponse.success();
+        case BOOL_VAL:
+            boolVal = (boolean) value.getValue();
+            return WriteResponse.success();
+        case TIME_VAL:
+            timeVal = (Date) value.getValue();
+            return WriteResponse.success();
+        case LINK_VAL:
+            linkVal = (ObjectLink) value.getValue();
+            return WriteResponse.success();
+        case OPAQUE_VAL:
+            opaqueVal = (byte[]) value.getValue();
+            return WriteResponse.success();
+        case INT_ARRAY_VAL:
+            if (replace) {
+                intArrayVal.clear();
+            }
+            if (value.isMultiInstances()) {
+                Map<Integer, LwM2mResourceInstance> intMap = value.getInstances();
+                for (Map.Entry<Integer, LwM2mResourceInstance> entry : intMap.entrySet()) {
+                    intArrayVal.put(entry.getKey(), (Long) entry.getValue().getValue());
+                }
+                return WriteResponse.success();
+            }
+            return WriteResponse.badRequest("expected array");
+        case FLOAT_ARRAY_VAL:
+            if (replace) {
+                floatArrayVal.clear();
+            }
+            if (value.isMultiInstances()) {
+                Map<Integer, LwM2mResourceInstance> intMap = value.getInstances();
+                for (Map.Entry<Integer, LwM2mResourceInstance> entry : intMap.entrySet()) {
+                    floatArrayVal.put(entry.getKey(), (double) entry.getValue().getValue());
+                }
+                return WriteResponse.success();
+            }
+            return WriteResponse.badRequest("expected array");
+        case STRING_ARRAY_VAL:
+            if (replace) {
+                stringArrayVal.clear();
+            }
+            if (value.isMultiInstances()) {
+                Map<Integer, LwM2mResourceInstance> intMap = value.getInstances();
+                for (Map.Entry<Integer, LwM2mResourceInstance> entry : intMap.entrySet()) {
+                    stringArrayVal.put(entry.getKey(), (String) entry.getValue().getValue());
+                }
+                return WriteResponse.success();
+            }
+            return WriteResponse.badRequest("expected array");
+        case BOOL_ARRAY_VAL:
+            if (replace) {
+                boolArrayVal.clear();
+            }
+            if (value.isMultiInstances()) {
+                Map<Integer, LwM2mResourceInstance> intMap = value.getInstances();
+                for (Map.Entry<Integer, LwM2mResourceInstance> entry : intMap.entrySet()) {
+                    boolArrayVal.put(entry.getKey(), (boolean) entry.getValue().getValue());
+                }
+                return WriteResponse.success();
+            }
+            return WriteResponse.badRequest("expected array");
+        case TIME_ARRAY_VAL:
+            if (replace) {
+                timeArrayVal.clear();
+            }
+            if (value.isMultiInstances()) {
+                Map<Integer, LwM2mResourceInstance> intMap = value.getInstances();
+                for (Map.Entry<Integer, LwM2mResourceInstance> entry : intMap.entrySet()) {
+                    timeArrayVal.put(entry.getKey(), (Date) entry.getValue().getValue());
+                }
+                return WriteResponse.success();
+            }
+            return WriteResponse.badRequest("expected array");
+        case LINK_ARRAY_VAL:
+            if (replace) {
+                linkArrayVal.clear();
+            }
+            if (value.isMultiInstances()) {
+                Map<Integer, LwM2mResourceInstance> intMap = value.getInstances();
+                for (Map.Entry<Integer, LwM2mResourceInstance> entry : intMap.entrySet()) {
+                    linkArrayVal.put(entry.getKey(), (ObjectLink) entry.getValue().getValue());
+                }
+                return WriteResponse.success();
+            }
+            return WriteResponse.badRequest("expected array");
+        case OPAQUE_ARRAY_VAL:
+            if (replace) {
+                opaqueArrayVal.clear();
+            }
+            if (value.isMultiInstances()) {
+                Map<Integer, LwM2mResourceInstance> intMap = value.getInstances();
+                for (Map.Entry<Integer, LwM2mResourceInstance> entry : intMap.entrySet()) {
+                    opaqueArrayVal.put(entry.getKey(), (byte[]) entry.getValue().getValue());
+                }
+                return WriteResponse.success();
+            }
+            return WriteResponse.badRequest("expected array");
+        default:
+            return WriteResponse.notFound();
+        }
+    }
+
+    private void adjustValues() {
+        floatVal += 0.5d;
+        intVal += 1;
+        fireResourcesChange(INT_VAL, FLOAT_VAL);
+    }
+
+    @Override
+    public List<Integer> getAvailableResourceIds(ObjectModel model) {
+        return supportedResources;
+    }
+
+    @Override
+    public void destroy() {
+        scheduler.shutdown();
+    }
+}
\ No newline at end of file
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadCompositeTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadCompositeTest.java
index 31e25e47..2778a9d5 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadCompositeTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadCompositeTest.java
@@ -98,13 +98,13 @@ public class ReadCompositeTest {
     public void can_read_resource_instance() throws InterruptedException {
         // read device model number
         ReadCompositeResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new ReadCompositeRequest(requestContentFormat, responseContentFormat, "/2000/0/10/1"));
+                new ReadCompositeRequest(requestContentFormat, responseContentFormat, "/2000/0/65010/1"));
 
         // verify result
         assertEquals(CONTENT, response.getCode());
         assertContentFormat(responseContentFormat, response);
 
-        LwM2mResourceInstance resource = (LwM2mResourceInstance) response.getContent("/2000/0/10/1");
+        LwM2mResourceInstance resource = (LwM2mResourceInstance) response.getContent("/2000/0/65010/1");
         assertEquals(1, resource.getId());
         assertEquals(Type.STRING, resource.getType());
 
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java
index 05de5ed2..04caa478 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java
@@ -117,5 +117,4 @@ public class ReadMultiValueTest {
         LwM2mObjectInstance instance = (LwM2mObjectInstance) response.getContent();
         assertEquals(0, instance.getId());
     }
-
 }
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java
index 2c3d1554..d505360c 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java
@@ -83,7 +83,7 @@ public class IntegrationTestHelper {
     public static final int OBJLNK_SINGLE_INSTANCE_RESOURCE_ID = 7;
     public static final int INTEGER_MANDATORY_RESOURCE_ID = 8;
     public static final int STRING_MANDATORY_RESOURCE_ID = 9;
-    public static final int STRING_RESOURCE_INSTANCE_ID = 10;
+    public static final int STRING_RESOURCE_INSTANCE_ID = 65010;
     public static final int UNSIGNED_INTEGER_RESOURCE_ID = 11;
 
     public static final String MULTI_INSTANCE = "multiinstance";
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java
index 17066b8b..204e9c9c 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java
@@ -15,6 +15,8 @@
  *******************************************************************************/
 package org.eclipse.leshan.integration.tests.write;
 
+import static org.eclipse.leshan.core.ResponseCode.CONTENT;
+import static org.eclipse.leshan.integration.tests.util.TestUtil.assertContentFormat;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.Is.is;
 import static org.hamcrest.core.IsInstanceOf.instanceOf;
@@ -208,4 +210,35 @@ public class WriteMultiValueTest {
         assertEquals(((ObjectLink) resource.getValue(2)).getObjectId(), 10244);
         assertEquals(((ObjectLink) resource.getValue(2)).getObjectInstanceId(), 3);
     }
+
+    @Test
+    public void can_write_object_resource_instance() throws InterruptedException {
+
+        Map<Integer, String> values = new HashMap<>();
+        values.put(10, "value10");
+        values.put(20, "value20");
+
+        // write multi instance
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, IntegrationTestHelper.TEST_OBJECT_ID, 0,
+                        IntegrationTestHelper.STRING_RESOURCE_INSTANCE_ID, values, Type.STRING));
+
+        // Verify Write result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read multi instance
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(contentFormat,
+                IntegrationTestHelper.TEST_OBJECT_ID, 0, IntegrationTestHelper.STRING_RESOURCE_INSTANCE_ID));
+
+        // verify result
+        assertEquals(CONTENT, readResponse.getCode());
+        assertContentFormat(contentFormat, readResponse);
+
+        LwM2mMultipleResource resourceInstance = (LwM2mMultipleResource) readResponse.getContent();
+        assertEquals("value10", resourceInstance.getInstance(10).getValue());
+        assertEquals("value20", resourceInstance.getInstance(20).getValue());
+    }
+
 }