Repository: eclipse/leshan
Pull request #795 (2020-01-27 16:59:23)

Make LeshanClient more custimizable
Add protected create method for each internal part of `LeshanClient` to allow to replace it.
(mainly to allow easy quick fix)


Patch:
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/CaliforniumEndpointsManager.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/CaliforniumEndpointsManager.java
index 01f59cd1..dac298a8 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/CaliforniumEndpointsManager.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/CaliforniumEndpointsManager.java
@@ -54,14 +54,14 @@ public class CaliforniumEndpointsManager implements EndpointsManager {
 
     private static final Logger LOG = LoggerFactory.getLogger(CaliforniumEndpointsManager.class);
 
-    private boolean started = false;
-
-    private CoapEndpoint currentEndpoint;
-    private Builder dtlsConfigbuilder;
-    private NetworkConfig coapConfig;
-    private InetSocketAddress localAddress;
-    private CoapServer coapServer;
-    private EndpointFactory endpointFactory;
+    protected boolean started = false;
+
+    protected CoapEndpoint currentEndpoint;
+    protected Builder dtlsConfigbuilder;
+    protected NetworkConfig coapConfig;
+    protected InetSocketAddress localAddress;
+    protected CoapServer coapServer;
+    protected EndpointFactory endpointFactory;
 
     public CaliforniumEndpointsManager(CoapServer coapServer, InetSocketAddress localAddress, NetworkConfig coapConfig,
             Builder dtlsConfigBuilder, EndpointFactory endpointFactory) {
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
index af482f7a..c88fc413 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
@@ -20,7 +20,9 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ScheduledExecutorService;
 
+import org.eclipse.californium.core.CoapResource;
 import org.eclipse.californium.core.CoapServer;
+import org.eclipse.californium.core.network.CoapEndpoint;
 import org.eclipse.californium.core.network.config.NetworkConfig;
 import org.eclipse.californium.core.server.resources.Resource;
 import org.eclipse.californium.elements.util.ExecutorsUtil;
@@ -47,94 +49,143 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * A Lightweight M2M client based on Californium (CoAP implementation).
+ * A Lightweight M2M client based on Californium (CoAP implementation) which supports only 1 LWM2M server.
  */
 public class LeshanClient implements LwM2mClient {
 
     private static final Logger LOG = LoggerFactory.getLogger(LeshanClient.class);
 
-    private final CoapServer clientSideServer;
+    private final CoapAPI coapApi;
+    private final CoapServer coapServer;
     private final CaliforniumLwM2mRequestSender requestSender;
-    private final RegistrationEngine engine;
+    private final CaliforniumEndpointsManager endpointsManager;
+
+    private LwM2mObjectTree objectTree;
     private final BootstrapHandler bootstrapHandler;
+    private final RegistrationEngine engine;
     private final LwM2mClientObserverDispatcher observers;
-    private LwM2mObjectTree objectTree;
-
-    private final CaliforniumEndpointsManager endpointsManager;
 
     public LeshanClient(String endpoint, InetSocketAddress localAddress,
             List<? extends LwM2mObjectEnabler> objectEnablers, NetworkConfig coapConfig, Builder dtlsConfigBuilder,
-            EndpointFactory endpointFactory, Map<String, String> additionalAttributes, final LwM2mNodeEncoder encoder,
-            final LwM2mNodeDecoder decoder, ScheduledExecutorService sharedExecutor) {
+            EndpointFactory endpointFactory, Map<String, String> additionalAttributes, LwM2mNodeEncoder encoder,
+            LwM2mNodeDecoder decoder, ScheduledExecutorService sharedExecutor) {
 
         Validate.notNull(endpoint);
         Validate.notEmpty(objectEnablers);
         Validate.notNull(coapConfig);
 
-        // create ObjectTree
-        objectTree = new LwM2mObjectTree(this, objectEnablers);
+        objectTree = createObjectTree(objectEnablers);
+        observers = createClientObserverDispatcher();
+        bootstrapHandler = createBoostrapHandler(objectTree);
+        coapServer = createCoapServer(coapConfig, sharedExecutor);
+        linkObjectTreeToCoapServer(coapServer, bootstrapHandler, objectTree, encoder, decoder);
+        coapServer.add(createBootstrapResource(bootstrapHandler));
+        endpointsManager = createEndpointsManager(coapServer, localAddress, coapConfig, dtlsConfigBuilder,
+                endpointFactory);
+        requestSender = createRequestSender(endpointsManager);
+        engine = createRegistrationEngine(endpoint, objectTree, endpointsManager, requestSender, bootstrapHandler,
+                observers, additionalAttributes, sharedExecutor);
+        createRegistrationUpdateHandler(engine, endpointsManager, bootstrapHandler, objectTree);
+
+        coapApi = new CoapAPI();
+    }
+
+    protected LwM2mObjectTree createObjectTree(List<? extends LwM2mObjectEnabler> objectEnablers) {
+        return new LwM2mObjectTree(this, objectEnablers);
+    }
 
-        // Create Client Observers
-        observers = new LwM2mClientObserverDispatcher();
+    protected LwM2mClientObserverDispatcher createClientObserverDispatcher() {
+        return new LwM2mClientObserverDispatcher();
+    }
 
-        bootstrapHandler = new BootstrapHandler(objectTree.getObjectEnablers());
+    protected BootstrapHandler createBoostrapHandler(LwM2mObjectTree objectTree) {
+        return new BootstrapHandler(objectTree.getObjectEnablers());
+    }
 
-        // Create CoAP Server
-        clientSideServer = new CoapServer(coapConfig) {
+    protected CoapServer createCoapServer(NetworkConfig coapConfig, ScheduledExecutorService sharedExecutor) {
+        // create coap server
+        CoapServer coapServer = new CoapServer(coapConfig) {
             @Override
             protected Resource createRoot() {
                 // Use to handle Delete on "/"
                 return new org.eclipse.leshan.client.californium.RootResource(bootstrapHandler, this);
             }
         };
+
+        // configure executors
         if (sharedExecutor != null) {
-            clientSideServer.setExecutors(sharedExecutor, sharedExecutor, true);
+            coapServer.setExecutors(sharedExecutor, sharedExecutor, true);
         } else {
             // use same executor as main and secondary one.
             ScheduledExecutorService executor = ExecutorsUtil.newScheduledThreadPool(
                     coapConfig.getInt(NetworkConfig.Keys.PROTOCOL_STAGE_THREAD_COUNT),
                     new NamedThreadFactory("CoapServer(main)#"));
-            clientSideServer.setExecutors(executor, executor, false);
+            coapServer.setExecutors(executor, executor, false);
         }
+        return coapServer;
+    }
+
+    protected void linkObjectTreeToCoapServer(final CoapServer coapServer, final BootstrapHandler bootstrapHandler,
+            LwM2mObjectTree objectTree, final LwM2mNodeEncoder encoder, final LwM2mNodeDecoder decoder) {
 
         // Create CoAP resources for each lwm2m Objects.
-        for (LwM2mObjectEnabler enabler : objectEnablers) {
-            ObjectResource clientObject = new ObjectResource(enabler, bootstrapHandler, encoder, decoder);
-            clientSideServer.add(clientObject);
+        for (LwM2mObjectEnabler enabler : objectTree.getObjectEnablers().values()) {
+            CoapResource clientObject = createObjectResource(enabler, bootstrapHandler, encoder, decoder);
+            coapServer.add(clientObject);
         }
+
+        // listen object tree
         objectTree.addListener(new ObjectsListenerAdapter() {
             @Override
             public void objectAdded(LwM2mObjectEnabler object) {
-                ObjectResource clientObject = new ObjectResource(object, bootstrapHandler, encoder, decoder);
-                clientSideServer.add(clientObject);
+                CoapResource clientObject = createObjectResource(object, bootstrapHandler, encoder, decoder);
+                coapServer.add(clientObject);
             }
 
             @Override
             public void objectRemoved(LwM2mObjectEnabler object) {
-                Resource resource = clientSideServer.getRoot().getChild(Integer.toString(object.getId()));
+                Resource resource = coapServer.getRoot().getChild(Integer.toString(object.getId()));
                 if (resource instanceof ObjectListener) {
                     object.removeListener((ObjectListener) (resource));
                 }
-                clientSideServer.remove(resource);
+                coapServer.remove(resource);
             }
         });
+    }
 
-        // Create CoAP resources needed for the bootstrap sequence
-        clientSideServer.add(new BootstrapResource(bootstrapHandler));
+    protected CoapResource createObjectResource(LwM2mObjectEnabler enabler, BootstrapHandler bootstrapHandler,
+            LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder) {
+        return new ObjectResource(enabler, bootstrapHandler, encoder, decoder);
+    }
 
-        // Create EndpointHandler
-        endpointsManager = new CaliforniumEndpointsManager(clientSideServer, localAddress, coapConfig,
-                dtlsConfigBuilder, endpointFactory);
+    protected CoapResource createBootstrapResource(BootstrapHandler bootstrapHandler) {
+        return new BootstrapResource(bootstrapHandler);
+    }
+
+    protected CaliforniumEndpointsManager createEndpointsManager(CoapServer coapServer, InetSocketAddress localAddress,
+            NetworkConfig coapConfig, Builder dtlsConfigBuilder, EndpointFactory endpointFactory) {
+        return new CaliforniumEndpointsManager(coapServer, localAddress, coapConfig, dtlsConfigBuilder,
+                endpointFactory);
+    }
 
-        // Create sender
-        requestSender = new CaliforniumLwM2mRequestSender(endpointsManager);
+    protected CaliforniumLwM2mRequestSender createRequestSender(CaliforniumEndpointsManager endpointsManager) {
+        return new CaliforniumLwM2mRequestSender(endpointsManager);
+    }
 
-        // Create registration engine
-        engine = new RegistrationEngine(endpoint, objectTree.getObjectEnablers(), endpointsManager, requestSender,
-                bootstrapHandler, observers, additionalAttributes, sharedExecutor);
+    protected RegistrationEngine createRegistrationEngine(String endpoint, LwM2mObjectTree objectTree,
+            CaliforniumEndpointsManager endpointsManager, CaliforniumLwM2mRequestSender requestSender,
+            BootstrapHandler bootstrapHandler, LwM2mClientObserverDispatcher observer,
+            Map<String, String> additionalAttributes, ScheduledExecutorService sharedExecutor) {
+        return new RegistrationEngine(endpoint, objectTree.getObjectEnablers(), endpointsManager, requestSender,
+                bootstrapHandler, observer, additionalAttributes, sharedExecutor);
+    }
 
+    protected RegistrationUpdateHandler createRegistrationUpdateHandler(RegistrationEngine engine,
+            CaliforniumEndpointsManager endpointsManager, BootstrapHandler bootstrapHandler,
+            LwM2mObjectTree objectTree) {
         RegistrationUpdateHandler registrationUpdateHandler = new RegistrationUpdateHandler(engine, bootstrapHandler);
         registrationUpdateHandler.listen(objectTree);
+        return registrationUpdateHandler;
     }
 
     @Override
@@ -164,38 +215,78 @@ public class LeshanClient implements LwM2mClient {
     }
 
     @Override
-    public void triggerRegistrationUpdate() {
-        engine.triggerRegistrationUpdate();
+    public LwM2mObjectTree getObjectTree() {
+        return objectTree;
     }
 
     @Override
-    public LwM2mObjectTree getObjectTree() {
-        return objectTree;
+    public void triggerRegistrationUpdate() {
+        engine.triggerRegistrationUpdate();
     }
 
-    public CoapServer getCoapServer() {
-        return clientSideServer;
+    /**
+     * A CoAP API, generally needed if you want to access to underlying CoAP layer.
+     */
+    public CoapAPI coap() {
+        return coapApi;
     }
 
-    public InetSocketAddress getAddress() {
-        return endpointsManager.getEndpoint(null).getAddress();
+    public class CoapAPI {
+        /**
+         * @return the underlying {@link CoapServer}
+         */
+        public CoapServer getServer() {
+            return coapServer;
+        }
+
+        /**
+         * Returns the current {@link CoapEndpoint} used to communicate with the server.
+         * <p>
+         * A different endpoint address should be used by connected server, so this method only make sense as this
+         * current implementation supports only one LWM2M server.
+         * 
+         * @return the {@link CoapEndpoint} used to communicate to LWM2M server.
+         */
+        public CoapEndpoint getEndpoint() {
+            return (CoapEndpoint) endpointsManager.getEndpoint(null);
+        }
     }
 
+    /**
+     * Add listener to observe client lifecycle (bootstrap, register, update, deregister).
+     */
     public void addObserver(LwM2mClientObserver observer) {
         observers.addObserver(observer);
     }
 
+    /**
+     * Remove the given {@link LwM2mClientObserver}.
+     */
     public void removeObserver(LwM2mClientObserver observer) {
         observers.removeObserver(observer);
     }
 
     /**
-     * Returns the current registration Id (meaningful only because this client implementation supports only one LWM2M
-     * server).
+     * Returns the current registration Id.
+     * <p>
+     * Client should have 1 registration Id by connected server, so this method only make sense current implementation
+     * supports only one LWM2M server.
      * 
      * @return the client registration Id or <code>null</code> if the client is not registered
      */
     public String getRegistrationId() {
         return engine.getRegistrationId();
     }
+
+    /**
+     * Returns the current {@link InetSocketAddress} use to communicate with the server.
+     * <p>
+     * A different endpoint/socket address should be used by connected server, so this method only make sense as this
+     * current implementation supports only one LWM2M server.
+     * 
+     * @return the address used to connect to the server or <code>null</code> if the client is not started.
+     */
+    public InetSocketAddress getAddress() {
+        return endpointsManager.getEndpoint(null).getAddress();
+    }
 }
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClientBuilder.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClientBuilder.java
index 147cd89d..e061374a 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClientBuilder.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClientBuilder.java
@@ -20,6 +20,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ScheduledExecutorService;
 
+import org.eclipse.californium.core.CoapServer;
+import org.eclipse.californium.core.network.CoapEndpoint;
 import org.eclipse.californium.core.network.config.NetworkConfig;
 import org.eclipse.californium.core.network.config.NetworkConfig.Keys;
 import org.eclipse.californium.elements.Connector;
@@ -274,6 +276,36 @@ public class LeshanClientBuilder {
             dtlsConfigBuilder.setSniEnabled(false);
         }
 
+        return createLeshanClient(endpoint, localAddress, objectEnablers, coapConfig, dtlsConfigBuilder,
+                endpointFactory, additionalAttributes, encoder, decoder, executor);
+    }
+
+    /**
+     * Create the <code>LeshanClient</code>.
+     * <p>
+     * You can extend <code>LeshanClientBuilder</code> and override this method to create a new builder which will be
+     * able to build an extended <code>LeshanClient </code>.
+     * <p>
+     * See all the setters of this builder for more documentation about parameters.
+     * 
+     * @param endpoint The endpoint name for this client.
+     * @param localAddress The local address used for unsecured connection.
+     * @param objectEnablers The list of object enablers. An enabler adds to support for a given LWM2M object to the
+     *        client.
+     * @param coapConfig The coap config used to create {@link CoapEndpoint} and {@link CoapServer}.
+     * @param dtlsConfigBuilder The dtls config used to create the {@link DTLSConnector}.
+     * @param endpointFactory The factory which will create the {@link CoapEndpoint}.
+     * @param additionalAttributes Some extra (out-of-spec) attributes to add to the register request.
+     * @param encoder used to encode request payload.
+     * @param decoder used to decode response payload.
+     * @param sharedExecutor an optional shared executor.
+     * 
+     * @return the new {@link LeshanClient}
+     */
+    protected LeshanClient createLeshanClient(String endpoint, InetSocketAddress localAddress,
+            List<? extends LwM2mObjectEnabler> objectEnablers, NetworkConfig coapConfig, Builder dtlsConfigBuilder,
+            EndpointFactory endpointFactory, Map<String, String> additionalAttributes, LwM2mNodeEncoder encoder,
+            LwM2mNodeDecoder decoder, ScheduledExecutorService sharedExecutor) {
         return new LeshanClient(endpoint, localAddress, objectEnablers, coapConfig, dtlsConfigBuilder, endpointFactory,
                 additionalAttributes, encoder, decoder, executor);
     }
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/RootResource.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/RootResource.java
index dc804046..484452c1 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/RootResource.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/RootResource.java
@@ -36,7 +36,7 @@ import org.eclipse.leshan.util.StringUtils;
  */
 public class RootResource extends LwM2mClientCoapResource {
 
-    private CoapServer coapServer;
+    protected CoapServer coapServer;
 
     public RootResource(BootstrapHandler bootstrapHandler, CoapServer coapServer) {
         super("", bootstrapHandler);
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/SecurityObjectPskStore.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/SecurityObjectPskStore.java
index 0a4cbe12..59b8851a 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/SecurityObjectPskStore.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/SecurityObjectPskStore.java
@@ -45,7 +45,7 @@ import org.slf4j.LoggerFactory;
 public class SecurityObjectPskStore implements PskStore {
     private static final Logger LOG = LoggerFactory.getLogger(SecurityObjectPskStore.class);
 
-    private final LwM2mObjectEnabler securityEnabler;
+    protected final LwM2mObjectEnabler securityEnabler;
 
     /**
      * Warning : The securityEnabler should not contains 2 or more entries with the same identity. This is not a LWM2M
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 226ce396..382168a8 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
@@ -73,9 +73,9 @@ import org.eclipse.leshan.core.response.WriteResponse;
  */
 public class ObjectResource extends LwM2mClientCoapResource implements ObjectListener {
 
-    private final LwM2mObjectEnabler nodeEnabler;
-    private final LwM2mNodeEncoder encoder;
-    private final LwM2mNodeDecoder decoder;
+    protected final LwM2mObjectEnabler nodeEnabler;
+    protected final LwM2mNodeEncoder encoder;
+    protected final LwM2mNodeDecoder decoder;
 
     public ObjectResource(LwM2mObjectEnabler nodeEnabler, BootstrapHandler bootstrapHandler, LwM2mNodeEncoder encoder,
             LwM2mNodeDecoder decoder) {
@@ -150,7 +150,7 @@ public class ObjectResource extends LwM2mClientCoapResource implements ObjectLis
         }
     }
 
-    private ContentFormat getContentFormat(DownlinkRequest<?> request, ContentFormat requestedContentFormat) {
+    protected ContentFormat getContentFormat(DownlinkRequest<?> request, ContentFormat requestedContentFormat) {
         if (requestedContentFormat != null) {
             // we already check before this content format is supported.
             return requestedContentFormat;
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ResourceObserveFilter.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ResourceObserveFilter.java
index bd84d22b..eaa553e6 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ResourceObserveFilter.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ResourceObserveFilter.java
@@ -25,7 +25,7 @@ public class ResourceObserveFilter implements ObserveRelationFilter {
 
     private static final Logger LOG = LoggerFactory.getLogger(ResourceObserveFilter.class);
 
-    private final String notifyURI;
+    protected final String notifyURI;
 
     public ResourceObserveFilter(String notifyURI) {
         this.notifyURI = notifyURI;
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CoapRequestBuilder.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CoapRequestBuilder.java
index e5ecefae..40257925 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CoapRequestBuilder.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CoapRequestBuilder.java
@@ -32,8 +32,8 @@ import org.eclipse.leshan.core.request.UplinkRequestVisitor;
 
 public class CoapRequestBuilder implements UplinkRequestVisitor {
 
-    private Request coapRequest;
-    private final InetSocketAddress serverAddress;
+    protected Request coapRequest;
+    protected final InetSocketAddress serverAddress;
 
     public CoapRequestBuilder(InetSocketAddress serverAddress) {
         this.serverAddress = serverAddress;
@@ -121,7 +121,7 @@ public class CoapRequestBuilder implements UplinkRequestVisitor {
         return coapRequest;
     }
 
-    private void buildRequestSettings() {
+    protected void buildRequestSettings() {
         coapRequest.setDestinationContext(new AddressEndpointContext(serverAddress));
     }
 }
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/LwM2mClientResponseBuilder.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/LwM2mClientResponseBuilder.java
index de3d7d3d..d7d5707c 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/LwM2mClientResponseBuilder.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/LwM2mClientResponseBuilder.java
@@ -33,9 +33,9 @@ import org.eclipse.leshan.core.response.UpdateResponse;
 
 public class LwM2mClientResponseBuilder<T extends LwM2mResponse> implements UplinkRequestVisitor {
 
-    private final Response coapResponse;
+    protected final Response coapResponse;
 
-    private LwM2mResponse lwM2mresponse;
+    protected LwM2mResponse lwM2mresponse;
 
     public LwM2mClientResponseBuilder(Response coapResponse) {
         this.coapResponse = coapResponse;
@@ -106,7 +106,7 @@ public class LwM2mClientResponseBuilder<T extends LwM2mResponse> implements Upli
         return (T) lwM2mresponse;
     }
 
-    private void handleUnexpectedResponseCode(LwM2mRequest<?> request, Response coapResponse) {
+    protected void handleUnexpectedResponseCode(LwM2mRequest<?> request, Response coapResponse) {
         throw new InvalidResponseException("Server returned unexpected response code [%s] for [%s]",
                 coapResponse.getCode(), request);
     }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/request/ServerIdentity.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/request/ServerIdentity.java
index 442e36df..968bd216 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/request/ServerIdentity.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/request/ServerIdentity.java
@@ -65,7 +65,7 @@ public class ServerIdentity extends Identity {
      * @param identity identity to be used
      * @param role related role
      */
-    private ServerIdentity(Identity identity, Role role) {
+    protected ServerIdentity(Identity identity, Role role) {
         super(identity);
         this.role = role;
     }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseObjectEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseObjectEnabler.java
index f1dae71c..5d987013 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseObjectEnabler.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseObjectEnabler.java
@@ -64,14 +64,20 @@ import org.eclipse.leshan.core.response.WriteResponse;
 public abstract class BaseObjectEnabler implements LwM2mObjectEnabler {
 
     protected final int id;
-    private final TransactionalObjectListener transactionalListener = new TransactionalObjectListener(this);
-    private final ObjectModel objectModel;
+    protected final TransactionalObjectListener transactionalListener;
+    protected final ObjectModel objectModel;
 
     private LwM2mClient lwm2mClient;
 
     public BaseObjectEnabler(int id, ObjectModel objectModel) {
         this.id = id;
         this.objectModel = objectModel;
+        this.transactionalListener = createTransactionListener();
+
+    }
+
+    protected TransactionalObjectListener createTransactionListener() {
+        return new TransactionalObjectListener(this);
     }
 
     @Override
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectTree.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectTree.java
index c8302a9d..95fd4133 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectTree.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectTree.java
@@ -28,9 +28,9 @@ import org.eclipse.leshan.client.resource.listener.ObjectsListener;
 
 public class LwM2mObjectTree {
 
-    private ObjectListener dispatcher = new ObjectListenerDispatcher();
-    private CopyOnWriteArrayList<ObjectsListener> listeners = new CopyOnWriteArrayList<>();
-    private ConcurrentHashMap<Integer, LwM2mObjectEnabler> objectEnablers = new ConcurrentHashMap<>();
+    protected ObjectListener dispatcher = new ObjectListenerDispatcher();
+    protected CopyOnWriteArrayList<ObjectsListener> listeners = new CopyOnWriteArrayList<>();
+    protected ConcurrentHashMap<Integer, LwM2mObjectEnabler> objectEnablers = new ConcurrentHashMap<>();
 
     public LwM2mObjectTree(LwM2mClient client, LwM2mObjectEnabler... enablers) {
         this(client, Arrays.asList(enablers));
@@ -88,7 +88,7 @@ public class LwM2mObjectTree {
         }
     }
 
-    private class ObjectListenerDispatcher implements ObjectListener {
+    protected class ObjectListenerDispatcher implements ObjectListener {
         @Override
         public void objectInstancesAdded(LwM2mObjectEnabler object, int... instanceIds) {
             for (ObjectsListener listener : listeners) {
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 903f40b6..38d73bb9 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
@@ -56,9 +56,9 @@ import org.eclipse.leshan.core.response.WriteResponse;
 
 public class ObjectEnabler extends BaseObjectEnabler {
 
-    private Map<Integer, LwM2mInstanceEnabler> instances;
-    private LwM2mInstanceEnablerFactory instanceFactory;
-    private ContentFormat defaultContentFormat;
+    protected Map<Integer, LwM2mInstanceEnabler> instances;
+    protected LwM2mInstanceEnablerFactory instanceFactory;
+    protected ContentFormat defaultContentFormat;
 
     public ObjectEnabler(int id, ObjectModel objectModel, Map<Integer, LwM2mInstanceEnabler> instances,
             LwM2mInstanceEnablerFactory instanceFactory, ContentFormat defaultContentFormat) {
@@ -372,7 +372,7 @@ public class ObjectEnabler extends BaseObjectEnabler {
         return BootstrapDeleteResponse.badRequest(String.format("unexcepted path %s", request.getPath()));
     }
 
-    private void listenInstance(LwM2mInstanceEnabler instance, final int instanceId) {
+    protected void listenInstance(LwM2mInstanceEnabler instance, final int instanceId) {
         instance.addResourceChangedListener(new ResourceChangedListener() {
             @Override
             public void resourcesChanged(int... resourceIds) {
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/TransactionalObjectListener.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/TransactionalObjectListener.java
index 0966cebe..1963a2c3 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/TransactionalObjectListener.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/TransactionalObjectListener.java
@@ -29,13 +29,13 @@ import org.eclipse.leshan.client.resource.listener.ObjectListener;
  * This class is not threadsafe.
  */
 public class TransactionalObjectListener implements ObjectListener {
-    private boolean inTransaction = false;
-    private List<Integer> instancesAdded = new ArrayList<>();
-    private List<Integer> instancesRemoved = new ArrayList<>();
-    private Map<Integer, List<Integer>> resourcesChangedByInstance = new HashMap<>();
+    protected boolean inTransaction = false;
+    protected List<Integer> instancesAdded = new ArrayList<>();
+    protected List<Integer> instancesRemoved = new ArrayList<>();
+    protected Map<Integer, List<Integer>> resourcesChangedByInstance = new HashMap<>();
 
-    private LwM2mObjectEnabler objectEnabler;
-    private List<ObjectListener> innerListeners = new ArrayList<ObjectListener>();
+    protected LwM2mObjectEnabler objectEnabler;
+    protected List<ObjectListener> innerListeners = new ArrayList<ObjectListener>();
 
     public TransactionalObjectListener(LwM2mObjectEnabler objectEnabler) {
         this.objectEnabler = objectEnabler;
@@ -61,7 +61,7 @@ public class TransactionalObjectListener implements ObjectListener {
         inTransaction = false;
     }
 
-    private void fireStoredEvents() {
+    protected void fireStoredEvents() {
         if (!instancesAdded.isEmpty())
             fireObjectInstancesAdded(toIntArray(instancesAdded));
         if (!instancesRemoved.isEmpty())
@@ -120,7 +120,7 @@ public class TransactionalObjectListener implements ObjectListener {
         }
     }
 
-    private int[] toIntArray(List<Integer> list) {
+    protected int[] toIntArray(List<Integer> list) {
         int[] ret = new int[list.size()];
         int i = 0;
         for (Integer e : list)


Test patch:
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java
index 98e3228e..3cda139a 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java
@@ -491,7 +491,7 @@ public class ObserveTest {
     }
 
     private Connector getConnector(LeshanClient client) {
-        CoapEndpoint endpoint = (CoapEndpoint) helper.client.getCoapServer().getEndpoint(helper.client.getAddress());
+        CoapEndpoint endpoint = (CoapEndpoint) helper.client.coap().getServer().getEndpoint(helper.client.getAddress());
         return endpoint.getConnector();
     }
 
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/SecurityTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/SecurityTest.java
index f6e5afe0..3ad2aeae 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/SecurityTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/SecurityTest.java
@@ -123,7 +123,7 @@ public class SecurityTest {
 
         // Create new session with new credentials at client side.
         // Get connector
-        Endpoint endpoint = helper.client.getCoapServer().getEndpoint(helper.client.getAddress());
+        Endpoint endpoint = helper.client.coap().getServer().getEndpoint(helper.client.getAddress());
         DTLSConnector connector = (DTLSConnector) ((CoapEndpoint) endpoint).getConnector();
         // Clear DTLS session to force new handshake
         connector.clearConnectionState();