Repository: eclipse/leshan
Pull request #1035 (2021-06-29 14:31:03)

Add Opaque encoding/decoding for resource instance


Patch:
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueDecoder.java
index b2c1b91c..ebaa571e 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueDecoder.java
@@ -20,6 +20,7 @@ import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.NodeDecoder;
@@ -30,8 +31,9 @@ public class LwM2mNodeOpaqueDecoder implements NodeDecoder {
     @Override
     public <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
             throws CodecException {
-        if (!path.isResource())
-            throw new CodecException("Invalid path %s : OpaqueDecoder decodes resource only", path);
+        if (!path.isResource() && !path.isResourceInstance())
+            throw new CodecException("Invalid path %s : OpaqueDecoder decodes resource OR resource instance only",
+                    path);
 
         ResourceModel desc = model.getResourceModel(path.getObjectId(), path.getResourceId());
         if (desc != null && desc.type != Type.OPAQUE) {
@@ -39,6 +41,13 @@ public class LwM2mNodeOpaqueDecoder implements NodeDecoder {
                     "Invalid content format [%s] for path [%s], OPAQUE can only be used for single OPAQUE resource",
                     desc.type, path);
         }
-        return (T) LwM2mSingleResource.newBinaryResource(path.getResourceId(), content != null ? content : new byte[0]);
+
+        if (path.isResource()) {
+            return (T) LwM2mSingleResource.newBinaryResource(path.getResourceId(),
+                    content != null ? content : new byte[0]);
+        } else {
+            return (T) LwM2mResourceInstance.newBinaryInstance(path.getResourceInstanceId(),
+                    content != null ? content : new byte[0]);
+        }
     }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueEncoder.java
index 21197678..c96d480f 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueEncoder.java
@@ -77,14 +77,20 @@ public class LwM2mNodeOpaqueEncoder implements NodeEncoder {
             if (rSpec != null && rSpec.type != Type.OPAQUE) {
                 throw new CodecException("Only single opaque resource can be encoded in opaque format. [%s]", path);
             }
-            LOG.trace("Encoding resource {} into text", resource);
+            LOG.trace("Encoding resource {} into Bytes(OPAQUE format)", resource);
             Object value = converter.convertValue(resource.getValue(), resource.getType(), Type.OPAQUE, path);
             encoded = (byte[]) value;
         }
 
         @Override
         public void visit(LwM2mResourceInstance instance) {
-            throw new UnsupportedOperationException("not yet implemented");
+            ResourceModel rSpec = model.getResourceModel(path.getObjectId(), path.getResourceId());
+            if (rSpec != null && rSpec.type != Type.OPAQUE) {
+                throw new CodecException("Only opaque resource instance can be encoded in opaque format. [%s]", path);
+            }
+            LOG.trace("Encoding resource instance {} into Bytes(OPAQUE format)", instance);
+            Object value = converter.convertValue(instance.getValue(), instance.getType(), Type.OPAQUE, path);
+            encoded = (byte[]) value;
         }
     }
 }


Test patch:
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 dac832e2..c733cdb9 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
@@ -85,6 +85,7 @@ public class IntegrationTestHelper {
     public static final int STRING_MANDATORY_RESOURCE_ID = 9;
     public static final int STRING_RESOURCE_INSTANCE_ID = 65010;
     public static final int UNSIGNED_INTEGER_RESOURCE_ID = 11;
+    public static final int OPAQUE_MULTI_INSTANCE_RESOURCE_ID = 12;
 
     public static final String MULTI_INSTANCE = "multiinstance";
 
@@ -129,9 +130,13 @@ public class IntegrationTestHelper {
         ResourceModel unsignedintegerfield = new ResourceModel(UNSIGNED_INTEGER_RESOURCE_ID, "unsigned", Operations.RW,
                 false, false, Type.UNSIGNED_INTEGER, null, null, null);
 
+        ResourceModel opaqueMultiField = new ResourceModel(OPAQUE_MULTI_INSTANCE_RESOURCE_ID, "opaque_multi",
+                Operations.RW, true, false, Type.OPAQUE, null, null, null);
+
         objectModels.add(new ObjectModel(TEST_OBJECT_ID, "testobject", null, ObjectModel.DEFAULT_VERSION, true, false,
                 stringfield, booleanfield, integerfield, floatfield, timefield, opaquefield, objlnkfield,
-                objlnkSinglefield, integermandatoryfield, stringmandatoryfield, multiInstance, unsignedintegerfield));
+                objlnkSinglefield, integermandatoryfield, stringmandatoryfield, multiInstance, unsignedintegerfield,
+                opaqueMultiField));
 
         return objectModels;
     }
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java
index 15c9dd1e..0a797e42 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java
@@ -23,10 +23,14 @@ import static org.junit.Assert.*;
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
 
 import org.eclipse.californium.core.coap.Response;
 import org.eclipse.leshan.core.ResponseCode;
+import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.ReadRequest;
 import org.eclipse.leshan.core.request.WriteRequest;
@@ -99,4 +103,50 @@ public class WriteOpaqueValueTest {
         LwM2mResource resource = (LwM2mResource) readResponse.getContent();
         assertArrayEquals(expectedvalue, (byte[]) resource.getValue());
     }
+
+    @Test
+    public void write_opaque_resource_instance() throws InterruptedException {
+        // write 2 resource instances using TLV
+        Map<Integer, byte[]> values = new HashMap<>();
+        byte[] firstExpectedvalue = new byte[] { 1, 2, 3 };
+        byte[] secondExpectedvalue = new byte[] { 4, 5, 6 };
+        values.put(2, firstExpectedvalue);
+        values.put(3, secondExpectedvalue);
+
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(), new WriteRequest(ContentFormat.TLV,
+                TEST_OBJECT_ID, 0, OPAQUE_MULTI_INSTANCE_RESOURCE_ID, values, Type.OPAQUE));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read first instance using parameter content format
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, OPAQUE_MULTI_INSTANCE_RESOURCE_ID, 2));
+        LwM2mResourceInstance resource = (LwM2mResourceInstance) readResponse.getContent();
+        assertArrayEquals(firstExpectedvalue, (byte[]) resource.getValue());
+
+        // read second instance using parameter content format
+        readResponse = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, OPAQUE_MULTI_INSTANCE_RESOURCE_ID, 3));
+        resource = (LwM2mResourceInstance) readResponse.getContent();
+        assertArrayEquals(secondExpectedvalue, (byte[]) resource.getValue());
+
+        // write second resource instance using parameter content format
+        byte[] newExpectedvalue = new byte[] { 7, 8, 9, 10 };
+        response = helper.server.send(helper.getCurrentRegistration(), new WriteRequest(contentFormat, TEST_OBJECT_ID,
+                0, OPAQUE_MULTI_INSTANCE_RESOURCE_ID, 3, newExpectedvalue, Type.OPAQUE));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read second instance using parameter content format
+        readResponse = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, OPAQUE_MULTI_INSTANCE_RESOURCE_ID, 3));
+        resource = (LwM2mResourceInstance) readResponse.getContent();
+        assertArrayEquals(newExpectedvalue, (byte[]) resource.getValue());
+    }
 }