Repository: eclipse/leshan
Pull request #670 (2019-03-22 15:00:06)

Make configurable delete request for Bootstrap Server
Before this PR, bootstrap server always starts with delete request on "/".
Now in `BootstrapConfig`, there is a `toDelete` list of path to object to delete.


Patch:
diff --git a/leshan-bsserver-demo/src/main/resources/webapp/js/bsconfigstore.js b/leshan-bsserver-demo/src/main/resources/webapp/js/bsconfigstore.js
index b0ce807c..38f77ae6 100644
--- a/leshan-bsserver-demo/src/main/resources/webapp/js/bsconfigstore.js
+++ b/leshan-bsserver-demo/src/main/resources/webapp/js/bsconfigstore.js
@@ -1,6 +1,6 @@
-// convert config from server format to UI format :
+// convert config from rest API format to UI format :
 // we regroup security and server data
-var convertConfig = function(config){
+var configFromRestToUI = function(config){
     var newConfig = {dm:[],bs:[]};
     for (var i in config.security){
         var security = config.security[i];
@@ -23,16 +23,16 @@ var convertConfig = function(config){
     }
     return newConfig;
 };
-var convertConfigs = function(configs){
+var configsFromRestToUI = function(configs){
     var newConfigs = {};
     for (var endpoint in configs){
-        newConfigs[endpoint] = convertConfig(configs[endpoint]);
+        newConfigs[endpoint] = configFromRestToUI(configs[endpoint]);
     }
     return newConfigs;
 };
 
-//convert config from UI to UI server:
-var toConfig = function(config){
+//convert config from UI to rest API format:
+var configFromUIToRest = function(config){
     var newConfig = {servers:{}, security:{}};
     for (var i = 0; i < config.bs.length; i++) {
         var bs = config.bs[i];
@@ -44,6 +44,7 @@ var toConfig = function(config){
         delete dm.security;
         newConfig.servers[j] = dm;
     }
+    newConfig.toDelete = ["/0", "/1"]
     return newConfig;
 };
 
@@ -57,7 +58,7 @@ function BsConfigStore() {
 
     self.init = function (){
         $.get('api/bootstrap', function(data) {
-            self.bsconfigs = convertConfigs(data);
+            self.bsconfigs = configsFromRestToUI(data);
             self.trigger('changed', self.bsconfigs);
         }).fail(function(xhr, status, error){
             var err = "Unable to get the bootstrap info list";
@@ -67,15 +68,14 @@ function BsConfigStore() {
     };
 
     self.add = function(endpoint,config) {
-        var data = toConfig(config);
-        console.log(data);
+        var data = configFromUIToRest(config);
         $.ajax({
             type: "POST",
             url: 'api/bootstrap/'+endpoint,
             data: JSON.stringify(data),
             contentType:"application/json; charset=utf-8",
         }).done(function () {
-            self.bsconfigs[endpoint] = convertConfig(data);
+            self.bsconfigs[endpoint] = configFromRestToUI(data);
             self.trigger('changed', self.bsconfigs);
         }).fail(function (xhr, status, error) {
           var err = "Unable to post the bootstrap config";
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java
index 3ee9a80c..311726b5 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java
@@ -16,8 +16,10 @@
 package org.eclipse.leshan.server.bootstrap;
 
 import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.eclipse.leshan.SecurityMode;
@@ -29,6 +31,8 @@ import org.eclipse.leshan.core.request.BindingMode;
 @SuppressWarnings("serial")
 public class BootstrapConfig implements Serializable {
 
+    public List<String> toDelete = new ArrayList<>();
+
     public Map<Integer, ServerConfig> servers = new HashMap<>();
 
     public Map<Integer, ServerSecurity> security = new HashMap<>();
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java
index 09850601..be6190f4 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java
@@ -114,26 +114,34 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
     }
 
     protected void startBootstrap(BootstrapSession session, BootstrapConfig cfg) {
-        delete(session, cfg);
+        delete(session, cfg, new ArrayList<>(cfg.toDelete));
     }
 
-    protected void delete(final BootstrapSession session, final BootstrapConfig cfg) {
+    protected void delete(final BootstrapSession session, final BootstrapConfig cfg, final List<String> pathToDelete) {
+        if (!pathToDelete.isEmpty()) {
+            // get next Security configuration
+            String path = pathToDelete.remove(0);
 
-        final BootstrapDeleteRequest deleteRequest = new BootstrapDeleteRequest();
-        send(session, deleteRequest, new ResponseCallback<BootstrapDeleteResponse>() {
-            @Override
-            public void onResponse(BootstrapDeleteResponse response) {
-                LOG.trace("Bootstrap delete {} return code {}", session.getEndpoint(), response.getCode());
-                List<Integer> instancesToWrite = new ArrayList<>(cfg.security.keySet());
-                writeSecurities(session, cfg, instancesToWrite);
-            }
-        }, new ErrorCallback() {
-            @Override
-            public void onError(Exception e) {
-                LOG.debug(String.format("Error during bootstrap delete '/' on %s", session.getEndpoint()), e);
-                sessionManager.failed(session, DELETE_FAILED, deleteRequest);
-            }
-        });
+            final BootstrapDeleteRequest deleteRequest = new BootstrapDeleteRequest(path);
+            send(session, deleteRequest, new ResponseCallback<BootstrapDeleteResponse>() {
+                @Override
+                public void onResponse(BootstrapDeleteResponse response) {
+                    LOG.trace("Bootstrap delete {} return code {}", session.getEndpoint(), response.getCode());
+
+                    delete(session, cfg, pathToDelete);
+                }
+            }, new ErrorCallback() {
+                @Override
+                public void onError(Exception e) {
+                    LOG.debug(String.format("Error during bootstrap delete '/' on %s", session.getEndpoint()), e);
+                    sessionManager.failed(session, DELETE_FAILED, deleteRequest);
+                }
+            });
+        } else {
+            // we are done, write the securities now
+            List<Integer> securityInstancesToWrite = new ArrayList<>(cfg.security.keySet());
+            writeSecurities(session, cfg, securityInstancesToWrite);
+        }
     }
 
     protected void writeSecurities(final BootstrapSession session, final BootstrapConfig cfg,


Test patch:
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapIntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapIntegrationTestHelper.java
index 3da445eb..170808bd 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapIntegrationTestHelper.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapIntegrationTestHelper.java
@@ -41,9 +41,7 @@ import org.eclipse.leshan.client.californium.LeshanClientBuilder;
 import org.eclipse.leshan.client.object.Device;
 import org.eclipse.leshan.client.object.Security;
 import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
-import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
 import org.eclipse.leshan.client.resource.ObjectsInitializer;
-import org.eclipse.leshan.client.resource.SimpleInstanceEnabler;
 import org.eclipse.leshan.core.request.Identity;
 import org.eclipse.leshan.server.bootstrap.BootstrapConfig;
 import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ACLConfig;
@@ -122,25 +120,26 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
         createBootstrapServer(securityStore, null);
     }
 
-    @Override
-    public void createClient() {
+    public Security withoutSecurity() {
         // Create Security Object (with bootstrap server only)
         String bsUrl = "coap://" + bootstrapServer.getUnsecuredAddress().getHostString() + ":"
                 + bootstrapServer.getUnsecuredAddress().getPort();
-        Security security = new Security(bsUrl, true, 3, new byte[0], new byte[0], new byte[0], 12345);
+        return new Security(bsUrl, true, 3, new byte[0], new byte[0], new byte[0], 12345);
+    }
 
-        createClient(security);
+    @Override
+    public void createClient() {
+        createClient(withoutSecurity(), null);
     }
 
     public void createPSKClient(String pskIdentity, byte[] pskKey) {
-
         // Create Security Object (with bootstrap server only)
         String bsUrl = "coaps://" + bootstrapServer.getSecuredAddress().getHostString() + ":"
                 + bootstrapServer.getSecuredAddress().getPort();
         byte[] pskId = pskIdentity.getBytes(StandardCharsets.UTF_8);
         Security security = Security.pskBootstrap(bsUrl, pskId, pskKey);
 
-        createClient(security);
+        createClient(security, null);
     }
 
     @Override
@@ -150,23 +149,26 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
         Security security = Security.rpkBootstrap(bsUrl, clientPublicKey.getEncoded(), clientPrivateKey.getEncoded(),
                 bootstrapServerPublicKey.getEncoded());
 
-        createClient(security);
+        createClient(security, null);
     }
 
-    private void createClient(Security security) {
-        ObjectsInitializer initializer = new ObjectsInitializer();
+    public void createClient(Security security, ObjectsInitializer initializer) {
+        if (initializer == null) {
+            initializer = new ObjectsInitializer();
+        }
 
         // Initialize LWM2M Object Tree
         initializer.setInstancesForObject(LwM2mId.SECURITY, security);
         initializer.setInstancesForObject(LwM2mId.DEVICE,
                 new Device("Eclipse Leshan", IntegrationTestHelper.MODEL_NUMBER, "12345", "U"));
-        initializer.setClassForObject(LwM2mId.ACCESS_CONTROL, SimpleInstanceEnabler.class);
         initializer.setClassForObject(LwM2mId.SERVER, DummyInstanceEnabler.class);
-        List<LwM2mObjectEnabler> objects = initializer.createAll();
+        createClient(initializer);
+    }
 
+    public void createClient(ObjectsInitializer initializer) {
         // Create Leshan Client
         LeshanClientBuilder builder = new LeshanClientBuilder(getCurrentEndpoint());
-        builder.setObjects(objects);
+        builder.setObjects(initializer.createAll());
         client = builder.build();
         setupClientMonitoring();
     }
@@ -262,6 +264,28 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
         };
     }
 
+    public BootstrapStore deleteSecurityStore(Integer... objectToDelete) {
+        String[] pathToDelete = new String[objectToDelete.length];
+        for (int i = 0; i < pathToDelete.length; i++) {
+            pathToDelete[i] = "/" + objectToDelete[i];
+
+        }
+        return deleteSecurityStore(pathToDelete);
+    }
+
+    public BootstrapStore deleteSecurityStore(final String... pathToDelete) {
+        return new BootstrapStore() {
+
+            @Override
+            public BootstrapConfig getBootstrap(String endpoint, Identity deviceIdentity) {
+
+                BootstrapConfig bsConfig = new BootstrapConfig();
+                bsConfig.toDelete = Arrays.asList(pathToDelete);
+                return bsConfig;
+            }
+        };
+    }
+
     public BootstrapStore unsecuredWithAclBootstrapStore() {
         return new BootstrapStore() {
 
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java
index 7d7e8af0..f9609d27 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java
@@ -19,8 +19,11 @@ import static org.eclipse.leshan.integration.tests.SecureIntegrationTestHelper.*
 import static org.hamcrest.CoreMatchers.hasItems;
 import static org.junit.Assert.*;
 
+import org.eclipse.leshan.LwM2mId;
 import org.eclipse.leshan.SecurityMode;
 import org.eclipse.leshan.client.request.ServerIdentity;
+import org.eclipse.leshan.client.resource.ObjectsInitializer;
+import org.eclipse.leshan.client.resource.SimpleInstanceEnabler;
 import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.request.ReadRequest;
@@ -70,6 +73,39 @@ public class BootstrapTest {
         helper.assertClientRegisterered();
     }
 
+    @Test
+    public void bootstrapDeleteSecurity() {
+        // Create DM Server without security & start it
+        helper.createServer();
+        helper.server.start();
+
+        // Create and start bootstrap server
+        helper.createBootstrapServer(null,
+                helper.deleteSecurityStore(LwM2mId.ACCESS_CONTROL, LwM2mId.CONNECTIVITY_STATISTICS));
+        helper.bootstrapServer.start();
+
+        // Create Client and check it is not already registered
+        ObjectsInitializer initializer = new ObjectsInitializer();
+        initializer.setInstancesForObject(LwM2mId.ACCESS_CONTROL, new SimpleInstanceEnabler());
+        initializer.setInstancesForObject(LwM2mId.CONNECTIVITY_STATISTICS, new SimpleInstanceEnabler());
+        helper.createClient(helper.withoutSecurity(), initializer);
+        helper.assertClientNotRegisterered();
+
+        // Start it and wait for bootstrap finished
+        helper.client.start();
+        helper.waitForBootstrapFinishedAtClientSide(1);
+
+        // ensure instances are deleted
+        ReadResponse response = helper.client.getObjectEnablers().get(LwM2mId.ACCESS_CONTROL)
+                .read(ServerIdentity.SYSTEM, new ReadRequest(LwM2mId.ACCESS_CONTROL));
+        assertTrue("ACL instance is not deleted", ((LwM2mObject) response.getContent()).getInstances().isEmpty());
+
+        response = helper.client.getObjectEnablers().get(LwM2mId.CONNECTIVITY_STATISTICS).read(ServerIdentity.SYSTEM,
+                new ReadRequest(LwM2mId.CONNECTIVITY_STATISTICS));
+        assertTrue("Connectvity instance is not deleted",
+                ((LwM2mObject) response.getContent()).getInstances().isEmpty());
+    }
+
     @Test
     public void bootstrapWithAcl() {
         // Create DM Server without security & start it
@@ -81,7 +117,9 @@ public class BootstrapTest {
         helper.bootstrapServer.start();
 
         // Create Client and check it is not already registered
-        helper.createClient();
+        ObjectsInitializer initializer = new ObjectsInitializer();
+        initializer.setInstancesForObject(LwM2mId.ACCESS_CONTROL, new SimpleInstanceEnabler());
+        helper.createClient(helper.withoutSecurity(), initializer);
         helper.assertClientNotRegisterered();
 
         // Start it and wait for registration
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/IntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/IntegrationTestHelper.java
index d6b90155..57123cad 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/IntegrationTestHelper.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/IntegrationTestHelper.java
@@ -222,6 +222,14 @@ public class IntegrationTestHelper {
         }
     }
 
+    public void waitForBootstrapFinishedAtClientSide(long timeInSeconds) {
+        try {
+            assertTrue(clientObserver.waitForBootstrap(timeInSeconds, TimeUnit.SECONDS));
+        } catch (InterruptedException | TimeoutException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     public void ensureNoUpdate(long timeInSeconds) {
         try {
             registrationListener.waitForUpdate(timeInSeconds, TimeUnit.SECONDS);