Repository: eclipse/leshan
Pull request #1037 (2021-06-30 13:23:57)

Add support of Partial Update to WRITE on multi-instance resources
(See https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues/527 for more details)


Patch:
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ObjectResource.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ObjectResource.java
index c64377c9..b0623432 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ObjectResource.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ObjectResource.java
@@ -289,15 +289,20 @@ public class ObjectResource extends LwM2mClientCoapResource implements ObjectLis
 
         // Manage Execute Request
         if (path.isResource()) {
-            byte[] payload = exchange.getRequestPayload();
-            ExecuteResponse response = nodeEnabler.execute(identity,
-                    new ExecuteRequest(URI, payload != null ? new String(payload) : null, coapRequest));
-            if (response.getCode().isError()) {
-                exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage());
-            } else {
-                exchange.respond(toCoapResponseCode(response.getCode()));
+            // execute request has no content format at all or a TEXT concent format for parameters.
+            if (!exchange.getRequestOptions().hasContentFormat()
+                    || ContentFormat.fromCode(exchange.getRequestOptions().getContentFormat()) == ContentFormat.TEXT) {
+                byte[] payload = exchange.getRequestPayload();
+
+                ExecuteResponse response = nodeEnabler.execute(identity,
+                        new ExecuteRequest(URI, payload != null ? new String(payload) : null, coapRequest));
+                if (response.getCode().isError()) {
+                    exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage());
+                } else {
+                    exchange.respond(toCoapResponseCode(response.getCode()));
+                }
+                return;
             }
-            return;
         }
 
         // handle content format for Write (Update) and Create request
@@ -313,6 +318,22 @@ public class ObjectResource extends LwM2mClientCoapResource implements ObjectLis
         }
         LwM2mModel model = new StaticModel(nodeEnabler.getObjectModel());
 
+        // manage partial update of multi-instance resource
+        if (path.isResource()) {
+            try {
+                LwM2mNode lwM2mNode = decoder.decode(exchange.getRequestPayload(), contentFormat, path, model);
+                WriteResponse response = nodeEnabler.write(identity,
+                        new WriteRequest(Mode.UPDATE, contentFormat, URI, lwM2mNode, coapRequest));
+                if (response.getCode().isError()) {
+                    exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage());
+                } else {
+                    exchange.respond(toCoapResponseCode(response.getCode()));
+                }
+            } catch (CodecException e) {
+                handleInvalidRequest(exchange.advanced(), "Unable to decode payload on WRITE", e);
+            }
+            return;
+        }
         // Manage Update Instance
         if (path.isObjectInstance()) {
             try {
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java
index 2de0be13..ecdfe3d0 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java
@@ -278,7 +278,8 @@ public class ObjectEnabler extends BaseObjectEnabler implements Destroyable, Sta
 
         // Manage Resource case
         if (path.getResourceInstanceId() == null) {
-            return instance.write(identity, true, path.getResourceId(), (LwM2mResource) request.getNode());
+            return instance.write(identity, request.isReplaceRequest(), path.getResourceId(),
+                    (LwM2mResource) request.getNode());
         }
 
         // Manage Resource Instance case
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteRequest.java
index d49cde3f..0663a4b2 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteRequest.java
@@ -299,23 +299,25 @@ public class WriteRequest extends AbstractSimpleDownlinkRequest<WriteResponse> {
     }
 
     /**
-     * Request to write a specific resource instance from a client.
+     * Request to write a <b>Multi-Instance Resource</b> using the TLV content format.
      *
      * @param mode the mode of the request : replace or update.
      * @param contentFormat Format of the payload (TLV or JSON).
-     * @param objectId the object ID of the resource
-     * @param objectInstanceId the object instance ID
-     * @param resourceId the (individual) resource's ID
-     * @param resourceInstanceId the resource instance's ID
-     * @param value the resource instance (id-&gt;value) to write.
+     * @param objectId the id of the object to write.
+     * @param objectInstanceId the id of the object instance to write.
+     * @param resourceId the id of the resource to write.
+     * @param values the list of resource instance (id-&gt;value) to write.
      * @param type the data type of the resource.
+     * @exception InvalidRequestException if parameters are invalid.
      */
     public WriteRequest(Mode mode, ContentFormat contentFormat, int objectId, int objectInstanceId, int resourceId,
-            int resourceInstanceId, Object value, Type type) {
-        this(mode, contentFormat, new LwM2mPath(objectId, objectInstanceId, resourceId, resourceInstanceId),
-                LwM2mResourceInstance.newInstance(resourceInstanceId, value, type), null);
+            Map<Integer, ?> values, Type type) throws InvalidRequestException {
+        this(mode, ContentFormat.TLV, new LwM2mPath(objectId, objectInstanceId, resourceId),
+                LwM2mMultipleResource.newResource(resourceId, values, type), null);
     }
 
+    // ***************** write resource instance****************** //
+
     /**
      * Request to write a specific resource instance from a client, MODE = REPLACE.
      *
@@ -387,7 +389,8 @@ public class WriteRequest extends AbstractSimpleDownlinkRequest<WriteResponse> {
             throw new InvalidRequestException("new node value is mandatory for %s", target);
 
         // Validate Mode
-        if (getPath().isResource() && mode == Mode.UPDATE)
+        if (getPath().isResource() && mode == Mode.UPDATE
+                && (node instanceof LwM2mSingleResource || node instanceof LwM2mResourceInstance))
             throw new InvalidRequestException("Invalid mode for '%s': update is not allowed on resource", target);
 
         // Validate node and path coherence


Test patch:
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 204e9c9c..73a76401 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
@@ -214,13 +214,15 @@ public class WriteMultiValueTest {
     @Test
     public void can_write_object_resource_instance() throws InterruptedException {
 
+        // --------------------------------
+        // write (replace) multi instance
+        // --------------------------------
         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,
+                new WriteRequest(Mode.REPLACE, contentFormat, IntegrationTestHelper.TEST_OBJECT_ID, 0,
                         IntegrationTestHelper.STRING_RESOURCE_INSTANCE_ID, values, Type.STRING));
 
         // Verify Write result
@@ -239,6 +241,59 @@ public class WriteMultiValueTest {
         LwM2mMultipleResource resourceInstance = (LwM2mMultipleResource) readResponse.getContent();
         assertEquals("value10", resourceInstance.getInstance(10).getValue());
         assertEquals("value20", resourceInstance.getInstance(20).getValue());
-    }
 
+        // --------------------------------
+        // write (update) multi instance
+        // --------------------------------
+        Map<Integer, String> newValues = new HashMap<>();
+        newValues.put(20, "value200");
+        newValues.put(30, "value30");
+        response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(Mode.UPDATE, contentFormat, IntegrationTestHelper.TEST_OBJECT_ID, 0,
+                        IntegrationTestHelper.STRING_RESOURCE_INSTANCE_ID, newValues, Type.STRING));
+
+        // Verify Write result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read multi instance
+        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);
+
+        resourceInstance = (LwM2mMultipleResource) readResponse.getContent();
+        assertEquals("value200", resourceInstance.getInstance(20).getValue());
+        assertEquals("value30", resourceInstance.getInstance(30).getValue());
+        assertEquals("value10", resourceInstance.getInstance(10).getValue());
+
+        // --------------------------------
+        // write (replace) multi instance
+        // --------------------------------
+        newValues = new HashMap<>();
+        newValues.put(1, "value1");
+        response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(Mode.REPLACE, contentFormat, IntegrationTestHelper.TEST_OBJECT_ID, 0,
+                        IntegrationTestHelper.STRING_RESOURCE_INSTANCE_ID, newValues, Type.STRING));
+
+        // Verify Write result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read multi instance
+        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);
+
+        resourceInstance = (LwM2mMultipleResource) readResponse.getContent();
+        assertEquals(1, resourceInstance.getInstances().size());
+        assertEquals("value1", resourceInstance.getInstance(1).getValue());
+    }
 }