Repository: eclipse/leshan
Pull request #469 (2018-02-09 16:41:58)

Add Queue mode support at server side adding a PresenceService

sbernard31: I taked into account most of the remarks.
If no objection I plan to integrate this tomorrow in master.

Patch:
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/exception/ClientSleepingException.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/exception/ClientSleepingException.java
new file mode 100644
index 00000000..25f805a0
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/exception/ClientSleepingException.java
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * Copyright (c) 2017 RISE SICS AB.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     RISE SICS AB - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.request.exception;
+
+/**
+ * Exception indicating that the message was cancelled on the CoAP layer and any retries will be stopped.
+ */
+public class ClientSleepingException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    public ClientSleepingException(String message, Object... args) {
+        super(String.format(message, args));
+    }
+}
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServerBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServerBuilder.java
index 589b424d..d900989d 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServerBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServerBuilder.java
@@ -38,6 +38,7 @@ import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.observation.Observation;
+import org.eclipse.leshan.core.request.exception.ClientSleepingException;
 import org.eclipse.leshan.server.LwM2mServer;
 import org.eclipse.leshan.server.californium.impl.InMemoryRegistrationStore;
 import org.eclipse.leshan.server.californium.impl.LeshanServer;
@@ -86,6 +87,7 @@ public class LeshanServerBuilder {
 
     private boolean noSecuredEndpoint;
     private boolean noUnsecuredEndpoint;
+    private boolean noQueueMode = false;
 
     /**
      * <p>
@@ -305,6 +307,17 @@ public class LeshanServerBuilder {
         return this;
     }
 
+    /**
+     * deactivate PresenceService which tracks presence of devices using LWM2M Queue Mode. When Queue Mode is
+     * deactivated request is always sent immediately and {@link ClientSleepingException} will never be raised.
+     * Deactivate QueueMode can make sense if you want to handle it on your own or if you don't plan to support devices
+     * with queue mode.
+     */
+    public LeshanServerBuilder disableQueueModeSupport() {
+        this.noQueueMode = true;
+        return this;
+    }
+
     /**
      * The default Californium/CoAP {@link NetworkConfig} used by the builder.
      */
@@ -431,7 +444,8 @@ public class LeshanServerBuilder {
             if (endpointFactory != null) {
                 securedEndpoint = endpointFactory.createSecuredEndpoint(dtlsConfig, coapConfig, registrationStore);
             } else {
-                securedEndpoint = new CoapEndpoint(new DTLSConnector(dtlsConfig), coapConfig, registrationStore, null, new Lwm2mEndpointContextMatcher());
+                securedEndpoint = new CoapEndpoint(new DTLSConnector(dtlsConfig), coapConfig, registrationStore, null,
+                        new Lwm2mEndpointContextMatcher());
             }
         }
 
@@ -441,6 +455,6 @@ public class LeshanServerBuilder {
         }
 
         return new LeshanServer(unsecuredEndpoint, securedEndpoint, registrationStore, securityStore, authorizer,
-                modelProvider, encoder, decoder, coapConfig);
+                modelProvider, encoder, decoder, coapConfig, noQueueMode);
     }
 }
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanServer.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanServer.java
index ab456029..83ad9100 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanServer.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanServer.java
@@ -12,6 +12,7 @@
  * 
  * Contributors:
  *     Sierra Wireless - initial API and implementation
+ *     RISE SICS AB - added Queue Mode operation
  *******************************************************************************/
 package org.eclipse.leshan.server.californium.impl;
 
@@ -45,6 +46,10 @@ import org.eclipse.leshan.server.californium.LeshanServerBuilder;
 import org.eclipse.leshan.server.impl.RegistrationServiceImpl;
 import org.eclipse.leshan.server.model.LwM2mModelProvider;
 import org.eclipse.leshan.server.observation.ObservationService;
+import org.eclipse.leshan.server.queue.PresenceService;
+import org.eclipse.leshan.server.queue.PresenceServiceImpl;
+import org.eclipse.leshan.server.queue.PresenceStateListener;
+import org.eclipse.leshan.server.queue.QueueModeLwM2mRequestSender;
 import org.eclipse.leshan.server.registration.Registration;
 import org.eclipse.leshan.server.registration.RegistrationHandler;
 import org.eclipse.leshan.server.registration.RegistrationListener;
@@ -95,6 +100,8 @@ public class LeshanServer implements LwM2mServer {
 
     private final CoapEndpoint securedEndpoint;
 
+    private final PresenceServiceImpl presenceService;
+
     private final CaliforniumRegistrationStore registrationStore;
 
     /**
@@ -109,11 +116,12 @@ public class LeshanServer implements LwM2mServer {
      * @param decoder decoder used to decode response payload.
      * @param encoder encode used to encode request payload.
      * @param coapConfig the CoAP {@link NetworkConfig}.
+     * @param noQueueMode true to disable presenceService.
      */
     public LeshanServer(CoapEndpoint unsecuredEndpoint, CoapEndpoint securedEndpoint,
             CaliforniumRegistrationStore registrationStore, SecurityStore securityStore, Authorizer authorizer,
             LwM2mModelProvider modelProvider, LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder,
-            NetworkConfig coapConfig) {
+            NetworkConfig coapConfig, boolean noQueueMode) {
 
         Validate.notNull(registrationStore, "registration store cannot be null");
         Validate.notNull(authorizer, "authorizer cannot be null");
@@ -124,13 +132,13 @@ public class LeshanServer implements LwM2mServer {
 
         // Init services and stores
         this.registrationStore = registrationStore;
-        this.registrationService = new RegistrationServiceImpl(registrationStore);
+        registrationService = new RegistrationServiceImpl(registrationStore);
         this.securityStore = securityStore;
-        this.observationService = new ObservationServiceImpl(registrationStore, modelProvider, decoder);
+        observationService = new ObservationServiceImpl(registrationStore, modelProvider, decoder);
         this.modelProvider = modelProvider;
 
         // Cancel observations on client unregistering
-        this.registrationService.addListener(new RegistrationListener() {
+        registrationService.addListener(new RegistrationListener() {
 
             @Override
             public void updated(RegistrationUpdate update, Registration updatedRegistration,
@@ -182,8 +190,18 @@ public class LeshanServer implements LwM2mServer {
         coapServer.add(rdResource);
 
         // create sender
-        requestSender = new CaliforniumLwM2mRequestSender(endpoints, this.observationService, modelProvider, encoder,
-                decoder);
+        // notify applications of LWM2M client coming online/offline
+        if (noQueueMode) {
+            // if no queue mode, create a "simple" sender
+            requestSender = new CaliforniumLwM2mRequestSender(endpoints, observationService, modelProvider, encoder,
+                    decoder);
+            presenceService = null;
+        } else {
+            presenceService = new PresenceServiceImpl();
+            registrationService.addListener(new PresenceStateListener(presenceService));
+            requestSender = new QueueModeLwM2mRequestSender(presenceService,
+                    new CaliforniumLwM2mRequestSender(endpoints, observationService, modelProvider, encoder, decoder));
+        }
     }
 
     @Override
@@ -254,6 +272,11 @@ public class LeshanServer implements LwM2mServer {
         return this.observationService;
     }
 
+    @Override
+    public PresenceService getPresenceService() {
+        return this.presenceService;
+    }
+
     @Override
     public SecurityStore getSecurityStore() {
         return this.securityStore;
@@ -267,24 +290,28 @@ public class LeshanServer implements LwM2mServer {
     @Override
     public <T extends LwM2mResponse> T send(Registration destination, DownlinkRequest<T> request)
             throws InterruptedException {
+
         return requestSender.send(destination, request, DEFAULT_TIMEOUT);
     }
 
     @Override
     public <T extends LwM2mResponse> T send(Registration destination, DownlinkRequest<T> request, long timeout)
             throws InterruptedException {
+
         return requestSender.send(destination, request, timeout);
     }
 
     @Override
     public <T extends LwM2mResponse> void send(Registration destination, DownlinkRequest<T> request,
             ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
+
         requestSender.send(destination, request, DEFAULT_TIMEOUT, responseCallback, errorCallback);
     }
 
     @Override
     public <T extends LwM2mResponse> void send(Registration destination, DownlinkRequest<T> request, long timeout,
             ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
+
         requestSender.send(destination, request, timeout, responseCallback, errorCallback);
     }
 
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/LwM2mServer.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/LwM2mServer.java
index 2868e898..3a0cbf9c 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/LwM2mServer.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/LwM2mServer.java
@@ -17,6 +17,7 @@ package org.eclipse.leshan.server;
 
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.request.DownlinkRequest;
+import org.eclipse.leshan.core.request.exception.ClientSleepingException;
 import org.eclipse.leshan.core.request.exception.InvalidResponseException;
 import org.eclipse.leshan.core.request.exception.RequestCanceledException;
 import org.eclipse.leshan.core.request.exception.RequestRejectedException;
@@ -26,6 +27,8 @@ import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.core.response.ResponseCallback;
 import org.eclipse.leshan.server.model.LwM2mModelProvider;
 import org.eclipse.leshan.server.observation.ObservationService;
+import org.eclipse.leshan.server.queue.PresenceListener;
+import org.eclipse.leshan.server.queue.PresenceService;
 import org.eclipse.leshan.server.registration.Registration;
 import org.eclipse.leshan.server.registration.RegistrationService;
 import org.eclipse.leshan.server.security.SecurityStore;
@@ -69,9 +72,12 @@ public interface LwM2mServer {
      * @throws RequestRejectedException if the request is rejected by foreign peer.
      * @throws RequestCanceledException if the request is cancelled.
      * @throws InvalidResponseException if the response received is malformed.
+     * @throws ClientSleepingException if the client is sleeping and then the request cannot be sent. This exception
+     *         will never be raised if Queue Mode support is deactivate.
      */
-    <T extends LwM2mResponse> T send(Registration destination, DownlinkRequest<T> request) throws InterruptedException,
-            CodecException, InvalidResponseException, RequestCanceledException, RequestRejectedException;
+    <T extends LwM2mResponse> T send(Registration destination, DownlinkRequest<T> request)
+            throws InterruptedException, CodecException, InvalidResponseException, RequestCanceledException,
+            RequestRejectedException, ClientSleepingException;
 
     /**
      * Sends a Lightweight M2M request synchronously. Will block until a response is received from the remote client.
@@ -86,10 +92,12 @@ public interface LwM2mServer {
      * @throws RequestRejectedException if the request is rejected by foreign peer.
      * @throws RequestCanceledException if the request is cancelled.
      * @throws InvalidResponseException if the response received is malformed.
+     * @throws ClientSleepingException if the client is sleeping and then the request cannot be sent. This exception
+     *         will never be raised if Queue Mode support is deactivate.
      */
     <T extends LwM2mResponse> T send(Registration destination, DownlinkRequest<T> request, long timeout)
             throws InterruptedException, CodecException, InvalidResponseException, RequestCanceledException,
-            RequestRejectedException;
+            RequestRejectedException, ClientSleepingException;
 
     /**
      * Sends a Lightweight M2M request asynchronously.
@@ -107,9 +115,12 @@ public interface LwM2mServer {
      *        <li>or any other RuntimeException for unexpected issue.
      *        </ul>
      * @throws CodecException if request payload can not be encoded.
+     * @throws ClientSleepingException if the client is sleeping and then the request cannot be sent. This exception
+     *         will never be raised if Queue Mode support is deactivate.
      */
     <T extends LwM2mResponse> void send(Registration destination, DownlinkRequest<T> request,
-            ResponseCallback<T> responseCallback, ErrorCallback errorCallback) throws CodecException;
+            ResponseCallback<T> responseCallback, ErrorCallback errorCallback)
+            throws CodecException, ClientSleepingException;
 
     /**
      * Sends a Lightweight M2M request asynchronously.
@@ -128,9 +139,12 @@ public interface LwM2mServer {
      *        <li>or any other RuntimeException for unexpected issue.
      *        </ul>
      * @throws CodecException if request payload can not be encoded.
+     * @throws ClientSleepingException if the client is sleeping and then the request cannot be sent. This exception
+     *         will never be raised if Queue Mode support is deactivate.
      */
     <T extends LwM2mResponse> void send(Registration destination, DownlinkRequest<T> request, long timeout,
-            ResponseCallback<T> responseCallback, ErrorCallback errorCallback) throws CodecException;
+            ResponseCallback<T> responseCallback, ErrorCallback errorCallback)
+            throws CodecException, ClientSleepingException;
 
     /**
      * Get the registration service to access to registered clients. You can use this object for listening client
@@ -144,6 +158,12 @@ public interface LwM2mServer {
      */
     ObservationService getObservationService();
 
+    /**
+     * Get the Presence service to get the status of LWM2M clients connected with binding mode 'Q'. You can use this
+     * object to add {@link PresenceListener} to get notified when a device comes online or offline.
+     */
+    PresenceService getPresenceService();
+
     /**
      * Get the SecurityStore containing of security information.
      */
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/Presence.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/Presence.java
new file mode 100644
index 00000000..33f390d8
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/Presence.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Bosch Software Innovations GmbH and others.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ *
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ *
+ * Contributors:
+ *     Bosch Software Innovations GmbH - initial API
+ *******************************************************************************/
+package org.eclipse.leshan.server.queue;
+
+/**
+ * Possible states of a LWM2M client registered with Queue mode binding.
+ */
+public enum Presence {
+    /** LWM2M Client is reachable and messages can be sent **/
+    AWAKE,
+    /** LWM2M Client is not reachable and no messages can be sent **/
+    SLEEPING
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceListener.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceListener.java
new file mode 100644
index 00000000..b2972b8f
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceListener.java
@@ -0,0 +1,40 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Bosch Software Innovations GmbH and others.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ *
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ *
+ * Contributors:
+ *     Bosch Software Innovations GmbH - initial API
+ *******************************************************************************/
+package org.eclipse.leshan.server.queue;
+
+import org.eclipse.leshan.server.registration.Registration;
+
+/**
+ * A listener aware of the status of LWM2M Client using queue mode binding.
+ *
+ */
+public interface PresenceListener {
+    
+
+    /**
+     * This method is invoked when the LWM2M client with the given endpoint state changes to awake.
+     * 
+     * @param registration data of the lwm2m client.
+     */
+    void onAwake(Registration registration);
+
+    /**
+     * This method is invoked when the LWM2M client with the given endpoint state changes to sleeping.
+     * 
+     * @param registration data of the lwm2m client.
+     */
+    void onSleeping(Registration registration);
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceService.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceService.java
new file mode 100644
index 00000000..c4da6886
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceService.java
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Bosch Software Innovations GmbH and others.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ *
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ *
+ * Contributors:
+ *     Bosch Software Innovations GmbH - initial API
+ *     RISE SICS AB - added more features 
+ *******************************************************************************/
+package org.eclipse.leshan.server.queue;
+
+import org.eclipse.leshan.server.registration.Registration;
+
+/**
+ * Tracks the status of each LWM2M client registered with Queue mode binding. Also ensures that the
+ * {@link PresenceListener} are notified on state changes only for those LWM2M clients registered using Queue mode
+ * binding.
+ *
+ * @see Presence
+ */
+public interface PresenceService {
+
+    /**
+     * Add the listener to get notified when the LWM2M client goes online or offline.
+     * 
+     * @param listener target to notify
+     */
+    void addListener(PresenceListener listener);
+
+    /**
+     * Remove the listener previously added. This method has no effect if the given listener is not previously added.
+     * 
+     * @param listener target to be removed.
+     */
+    void removeListener(PresenceListener listener);
+
+    /**
+     * Returns the current state of a given LWM2M client registration.
+     * 
+     * @param registration the client's registration object.
+     * @return true if the status is {@link Presence#Awake}
+     */
+    boolean isClientAwake(Registration registration);
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceServiceImpl.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceServiceImpl.java
new file mode 100644
index 00000000..5c54e1f5
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceServiceImpl.java
@@ -0,0 +1,182 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Bosch Software Innovations GmbH and others.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ *
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ *
+ * Contributors:
+ *     Bosch Software Innovations GmbH - initial API
+ *     RISE SICS AB - added more features 
+ *******************************************************************************/
+package org.eclipse.leshan.server.queue;
+
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.leshan.server.registration.Registration;
+
+/**
+ * Tracks the status of each LWM2M client registered with Queue mode binding. Also ensures that the
+ * {@link PresenceListener} are notified on state changes only for those LWM2M clients registered using Queue mode
+ * binding.
+ * 
+ * @see Presence
+ */
+public final class PresenceServiceImpl implements PresenceService {
+
+    private final ConcurrentMap<String, PresenceStatus> clientStatusList = new ConcurrentHashMap<>();
+    private final List<PresenceListener> listeners = new CopyOnWriteArrayList<>();
+    ScheduledExecutorService clientTimersExecutor = Executors.newSingleThreadScheduledExecutor();
+
+    @Override
+    public void addListener(PresenceListener listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(PresenceListener listener) {
+        listeners.remove(listener);
+    }
+
+    @Override
+    public boolean isClientAwake(Registration registration) {
+        PresenceStatus presenceStatus = clientStatusList.get(registration.getEndpoint());
+        if (presenceStatus == null) {
+            return false;
+        }
+        return presenceStatus.isClientAwake();
+
+    }
+
+    /**
+     * Set the state of the client identified by registration as {@link Presence#AWAKE}
+     * 
+     * @param reg the client's registration object
+     */
+    public void setAwake(Registration reg) {
+        if (reg.usesQueueMode()) {
+            PresenceStatus status = new PresenceStatus();
+            PresenceStatus previous = clientStatusList.putIfAbsent(reg.getEndpoint(), status);
+            if (previous != null) {
+                // We already have a status for this reg.
+                status = previous;
+            }
+
+            boolean stateChanged = false;
+            synchronized (status) {
+                stateChanged = status.setAwake();
+                startClientAwakeTimer(reg, status);
+            }
+
+            if (stateChanged) {
+                for (PresenceListener listener : listeners) {
+                    listener.onAwake(reg);
+                }
+            }
+        }
+    }
+
+    /**
+     * Notify the listeners that the client state changed to {@link Presence#SLEEPING}. The state changes is produced
+     * inside {@link PresenceStatus} when the timer expires or when the client doesn't respond to a request.
+     * 
+     * @param reg the client's registration object
+     */
+    public void setSleeping(Registration reg) {
+        if (reg.usesQueueMode()) {
+            PresenceStatus status = clientStatusList.get(reg.getEndpoint());
+
+            if (status != null) {
+                boolean stateChanged = false;
+                synchronized (status) {
+                    stateChanged = status.setSleeping();
+                    stopClientAwakeTimer(reg);
+                }
+                if (stateChanged) {
+                    for (PresenceListener listener : listeners) {
+                        listener.onSleeping(reg);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Removes the {@link PresenceStatus} object associated with the client from the list.
+     * 
+     * @param reg the client's registration object.
+     */
+    public void removePresenceStatusObject(Registration reg) {
+        clientStatusList.remove(reg.getEndpoint());
+    }
+
+    /**
+     * Returns the {@link PresenceStatus} object associated with the given endpoint name.
+     * 
+     * @param reg The client's registration object.
+     * @return The {@link PresenceStatus} object.
+     */
+    private PresenceStatus getPresenceStatusObject(Registration reg) {
+        return clientStatusList.get(reg.getEndpoint());
+    }
+
+    /**
+     * Start or restart (if already started) the timer that handles the client wait before sleep time.
+     * 
+     * @param status
+     */
+    public void startClientAwakeTimer(final Registration reg, PresenceStatus clientPresenceStatus) {
+
+        int clientAwakeTime = clientPresenceStatus.getClientAwakeTime();
+        ScheduledFuture<?> clientScheduledFuture = clientPresenceStatus.getClientScheduledFuture();
+
+        if (clientAwakeTime != 0) {
+            if (clientScheduledFuture != null) {
+                clientScheduledFuture.cancel(true);
+            }
+            clientScheduledFuture = clientTimersExecutor.schedule(new Runnable() {
+
+                @Override
+                public void run() {
+                    if (isClientAwake(reg)) {
+                        setSleeping(reg);
+                    }
+                }
+            }, clientAwakeTime, TimeUnit.MILLISECONDS);
+            clientPresenceStatus.setClientExecutorFuture(clientScheduledFuture);
+        }
+
+    }
+
+    /**
+     * Stop the timer that handles the client wait before sleep time.
+     */
+    private void stopClientAwakeTimer(Registration reg) {
+        PresenceStatus clientPresenceStatus = getPresenceStatusObject(reg);
+        ScheduledFuture<?> clientScheduledFuture = clientPresenceStatus.getClientScheduledFuture();
+        if (clientScheduledFuture != null) {
+            clientScheduledFuture.cancel(true);
+        }
+    }
+
+    /**
+     * Called when the client doesn't respond to a request, for changing its state to SLEEPING
+     */
+    public void clientNotResponding(Registration reg) {
+        if (isClientAwake(reg)) {
+            setSleeping(reg);
+        }
+    }
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceStateListener.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceStateListener.java
new file mode 100644
index 00000000..c3f08661
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceStateListener.java
@@ -0,0 +1,60 @@
+/*******************************************************************************
+ * Copyright (c) 2017 RISE SICS AB.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     RISE SICS AB - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.queue;
+
+import java.util.Collection;
+
+import org.eclipse.leshan.core.observation.Observation;
+import org.eclipse.leshan.server.registration.Registration;
+import org.eclipse.leshan.server.registration.RegistrationListener;
+import org.eclipse.leshan.server.registration.RegistrationUpdate;
+
+/**
+ * Listener that controls the state of the client (awake/sleeping) It is in charge of sending all the queued messages
+ * when the client is awake (has sent an update message), and controlling the time the client is awake before going to
+ * sleep.
+ */
+
+public class PresenceStateListener implements RegistrationListener {
+
+    PresenceServiceImpl presenceService;
+
+    public PresenceStateListener(PresenceServiceImpl presenceService) {
+        this.presenceService = presenceService;
+    }
+
+    @Override
+    public void registered(Registration reg, Registration previousReg, Collection<Observation> previousObsersations) {
+        if (reg.usesQueueMode()) {
+            presenceService.setAwake(reg);
+        }
+    }
+
+    @Override
+    public void updated(RegistrationUpdate update, Registration updatedRegistration,
+            Registration previousRegistration) {
+        if (updatedRegistration.usesQueueMode()) {
+            presenceService.setAwake(updatedRegistration);
+        }
+
+    }
+
+    @Override
+    public void unregistered(Registration reg, Collection<Observation> observations, boolean expired,
+            Registration newReg) {
+        presenceService.removePresenceStatusObject(reg);
+    }
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceStatus.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceStatus.java
new file mode 100644
index 00000000..ec45a8d3
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceStatus.java
@@ -0,0 +1,127 @@
+/*******************************************************************************
+ * Copyright (c) 2017 RISE SICS AB.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     RISE SICS AB - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.queue;
+
+import java.util.concurrent.ScheduledFuture;
+
+/**
+ * Class that contains all the necessary elements to handle the queue mode. Every registration object that uses Queue
+ * Mode has a PresenceStatus object linked to it for handling this mode.
+ */
+
+public class PresenceStatus {
+
+    /* The state of the client: Awake or Sleeping */
+    private Presence state;
+
+    ScheduledFuture<?> clientScheduledFuture;
+
+    private int clientAwakeTime;
+
+    public PresenceStatus() {
+        this.state = Presence.SLEEPING;
+        this.clientAwakeTime = 93000; /* ms, default CoAP value */
+    }
+
+    public PresenceStatus(int clientAwakeTime) {
+        this.state = Presence.SLEEPING;
+        this.clientAwakeTime = clientAwakeTime; /* ms */
+    }
+
+    /* Client State Control */
+
+    /**
+     * Set the client state to awake. This should be called when an update message is received from the server. It
+     * starts the client awake timer.
+     * 
+     * @return true if the state was changed (previous state was {@link Presence#SLEEPING}
+     */
+    public boolean setAwake() {
+        if (state == Presence.SLEEPING) {
+            state = Presence.AWAKE;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Set the client state to sleeping. This should be called when the the time the client waits before going to sleep
+     * expires, or when the client is not responding. It also notifies the listeners inside the {@link PresenceService}.
+     * It stops the client awake timer.
+     */
+
+    /**
+     * Set the client state to sleeping. This should be called when the the time the client waits before going to sleep
+     * expires, or when the client is not responding. It also notifies the listeners inside the {@link PresenceService}.
+     * It stops the client awake timer.
+     * 
+     * @return true if the state was changed (previous state was {@link Presence#AWAKE}
+     */
+    public boolean setSleeping() {
+        if (state == Presence.AWAKE) {
+            state = Presence.SLEEPING;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Tells if the client is awake or not
+     * 
+     * @return true if the status is {@link Presence#Awake}
+     */
+    public boolean isClientAwake() {
+        return state == Presence.AWAKE;
+    }
+
+    /* Control of the time the client waits before going to sleep */
+    /**
+     * Get the time that the client stays awake after an update message or the last received request.
+     * 
+     * @return The client awake time.
+     */
+    public int getClientAwakeTime() {
+        return clientAwakeTime;
+    }
+
+    /**
+     * Sets the client awake time, in case it wants to be modified during run time.
+     * 
+     * @param clientAwakeTime
+     */
+    public void setClientAwakeTime(int clientAwakeTime) {
+        this.clientAwakeTime = clientAwakeTime;
+    }
+
+    /**
+     * Sets the client scheduled task future, in order to cancel it.
+     * 
+     * @param clientScheduledFuture the scheduled future of the task.
+     */
+    public void setClientExecutorFuture(ScheduledFuture<?> clientScheduledFuture) {
+        this.clientScheduledFuture = clientScheduledFuture;
+    }
+
+    /**
+     * Gets the client scheduled task future, in order to cancel it.
+     * 
+     * @return the client scheduled task future.
+     */
+    public ScheduledFuture<?> getClientScheduledFuture() {
+        return this.clientScheduledFuture;
+    }
+
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/QueueModeLwM2mRequestSender.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/QueueModeLwM2mRequestSender.java
new file mode 100644
index 00000000..97249696
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/QueueModeLwM2mRequestSender.java
@@ -0,0 +1,123 @@
+/*******************************************************************************
+ * Copyright (c) 2017 RISE SICS AB.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     RISE SICS AB - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.queue;
+
+import org.eclipse.leshan.core.request.DownlinkRequest;
+import org.eclipse.leshan.core.request.exception.ClientSleepingException;
+import org.eclipse.leshan.core.request.exception.TimeoutException;
+import org.eclipse.leshan.core.response.ErrorCallback;
+import org.eclipse.leshan.core.response.LwM2mResponse;
+import org.eclipse.leshan.core.response.ResponseCallback;
+import org.eclipse.leshan.server.registration.Registration;
+import org.eclipse.leshan.server.request.LwM2mRequestSender;
+import org.eclipse.leshan.util.Validate;
+
+public class QueueModeLwM2mRequestSender implements LwM2mRequestSender {
+
+    private PresenceServiceImpl presenceService;
+    private LwM2mRequestSender delegatedSender;
+
+    /**
+     * @param presenceService the presence service object for setting the client into {@link Presence#SLEEPING} when
+     *        request Timeout expires and into {@link Presence#Awake} when a response arrives.
+     * @param delegatedSender internal sender that it is used for sending the requests, using delegation.
+     */
+    public QueueModeLwM2mRequestSender(PresenceServiceImpl presenceService, LwM2mRequestSender delegatedSender) {
+        Validate.notNull(presenceService);
+        Validate.notNull(delegatedSender);
+
+        this.presenceService = presenceService;
+        this.delegatedSender = delegatedSender;
+    }
+
+    @Override
+    public <T extends LwM2mResponse> T send(final Registration destination, DownlinkRequest<T> request, long timeout)
+            throws InterruptedException {
+
+        // If the client does not use Q-Mode, just send
+        if (!destination.usesQueueMode()) {
+            return delegatedSender.send(destination, request, timeout);
+        }
+
+        // If the client uses Q-Mode...
+
+        // If the client is sleeping, warn the user and return
+        if (!presenceService.isClientAwake(destination)) {
+            throw new ClientSleepingException("The destination client is sleeping, request cannot be sent.");
+        }
+
+        // Use delegation to send the request
+        T response = delegatedSender.send(destination, request, timeout);
+        if (response != null) {
+
+            // Set the client awake. This will restart the timer.
+            presenceService.setAwake(destination);
+        } else {
+
+            // If the timeout expires, this means the client does not respond.
+            presenceService.clientNotResponding(destination);
+        }
+        // Wait for response, then return it
+        return response;
+    }
+
+    @Override
+    public <T extends LwM2mResponse> void send(final Registration destination, DownlinkRequest<T> request, long timeout,
+            final ResponseCallback<T> responseCallback, final ErrorCallback errorCallback) {
+
+        // If the client does not use Q-Mode, just send
+        if (!destination.usesQueueMode()) {
+            delegatedSender.send(destination, request, timeout, responseCallback, errorCallback);
+            return;
+        }
+
+        // If the client uses Q-Mode...
+
+        // If the client is sleeping, warn the user and return
+        if (!presenceService.isClientAwake(destination)) {
+            throw new ClientSleepingException("The destination client is sleeping, request cannot be sent.");
+        }
+
+        // Use delegation to send the request, with specific callbacks to perform Queue Mode operation
+        delegatedSender.send(destination, request, timeout, new ResponseCallback<T>() {
+            @Override
+            public void onResponse(T response) {
+                // Set the client awake. This will restart the timer.
+                presenceService.setAwake(destination);
+
+                // Call the user's callback
+                responseCallback.onResponse(response);
+            }
+        }, new ErrorCallback() {
+            @Override
+            public void onError(Exception e) {
+                if (e instanceof TimeoutException) {
+                    // If the timeout expires, this means the client does not respond.
+                    presenceService.clientNotResponding(destination);
+                }
+
+                // Call the user's callback
+                errorCallback.onError(e);
+            }
+        });
+
+    }
+
+    @Override
+    public void cancelPendingRequests(Registration registration) {
+        delegatedSender.cancelPendingRequests(registration);
+    }
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java
index 81f8d4d0..d2c1aa5a 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java
@@ -289,7 +289,7 @@ public class Registration implements Serializable {
     }
 
     public boolean usesQueueMode() {
-        return bindingMode.equals(BindingMode.UQ);
+        return bindingMode.equals(BindingMode.UQ) || bindingMode.equals(BindingMode.UQS);
     }
 
     @Override
diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ClientServlet.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ClientServlet.java
index 1eac36c4..5411ac34 100644
--- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ClientServlet.java
+++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ClientServlet.java
@@ -41,6 +41,7 @@ import org.eclipse.leshan.core.request.ObserveRequest;
 import org.eclipse.leshan.core.request.ReadRequest;
 import org.eclipse.leshan.core.request.WriteRequest;
 import org.eclipse.leshan.core.request.WriteRequest.Mode;
+import org.eclipse.leshan.core.request.exception.ClientSleepingException;
 import org.eclipse.leshan.core.request.exception.InvalidRequestException;
 import org.eclipse.leshan.core.request.exception.InvalidResponseException;
 import org.eclipse.leshan.core.request.exception.RequestCanceledException;
@@ -165,7 +166,8 @@ public class ClientServlet extends HttpServlet {
                 // get content format
                 String contentFormatParam = req.getParameter(FORMAT_PARAM);
                 ContentFormat contentFormat = contentFormatParam != null
-                        ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null;
+                        ? ContentFormat.fromName(contentFormatParam.toUpperCase())
+                        : null;
 
                 // create & process request
                 ReadRequest request = new ReadRequest(contentFormat, target);
@@ -181,7 +183,8 @@ public class ClientServlet extends HttpServlet {
     }
 
     private void handleException(Exception e, HttpServletResponse resp) throws IOException {
-        if (e instanceof InvalidRequestException || e instanceof CodecException) {
+        if (e instanceof InvalidRequestException || e instanceof CodecException
+                || e instanceof ClientSleepingException) {
             LOG.warn("Invalid request", e);
             resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
             resp.getWriter().append("Invalid request:").append(e.getMessage()).flush();
@@ -229,7 +232,8 @@ public class ClientServlet extends HttpServlet {
                 // get content format
                 String contentFormatParam = req.getParameter(FORMAT_PARAM);
                 ContentFormat contentFormat = contentFormatParam != null
-                        ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null;
+                        ? ContentFormat.fromName(contentFormatParam.toUpperCase())
+                        : null;
 
                 // create & process request
                 LwM2mNode node = extractLwM2mNode(target, req);
@@ -262,7 +266,8 @@ public class ClientServlet extends HttpServlet {
                     // get content format
                     String contentFormatParam = req.getParameter(FORMAT_PARAM);
                     ContentFormat contentFormat = contentFormatParam != null
-                            ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null;
+                            ? ContentFormat.fromName(contentFormatParam.toUpperCase())
+                            : null;
 
                     // create & process request
                     ObserveRequest request = new ObserveRequest(contentFormat, target);
@@ -306,7 +311,8 @@ public class ClientServlet extends HttpServlet {
                     // get content format
                     String contentFormatParam = req.getParameter(FORMAT_PARAM);
                     ContentFormat contentFormat = contentFormatParam != null
-                            ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null;
+                            ? ContentFormat.fromName(contentFormatParam.toUpperCase())
+                            : null;
 
                     // create & process request
                     LwM2mNode node = extractLwM2mNode(target, req);


Test patch:
diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/queue/PresenceServiceTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/queue/PresenceServiceTest.java
new file mode 100644
index 00000000..eccd6a84
--- /dev/null
+++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/queue/PresenceServiceTest.java
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Bosch Software Innovations GmbH and others.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ *
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ *
+ * Contributors:
+ *     Bosch Software Innovations GmbH - initial API
+ *******************************************************************************/
+package org.eclipse.leshan.server.queue;
+
+import static org.junit.Assert.*;
+
+import java.net.Inet4Address;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+import org.eclipse.leshan.core.request.BindingMode;
+import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.server.registration.Registration;
+import org.junit.Test;
+
+/**
+ * tests the implementation of {@link PresenceService}
+ *
+ */
+public class PresenceServiceTest {
+
+    private PresenceServiceImpl presenceService = new PresenceServiceImpl();
+
+    @Test
+    public void testSetOnlineForNonQueueMode() throws Exception {
+        Registration registration = givenASimpleClient();
+        presenceService.addListener(new PresenceListener() {
+
+            @Override
+            public void onAwake(Registration registration) {
+                fail("No invocation was expected");
+            }
+
+            @Override
+            public void onSleeping(Registration registration) {
+                fail("No invocation was expected");
+            }
+        });
+        presenceService.setAwake(registration);
+    }
+
+    @Test
+    public void testIsOnline() throws Exception {
+        Registration queueModeRegistration = givenASimpleClientWithQueueMode();
+
+        assertTrue(presenceService.isClientAwake(queueModeRegistration));
+        presenceService.setSleeping(queueModeRegistration);
+        assertFalse(presenceService.isClientAwake(queueModeRegistration));
+    }
+
+    private Registration givenASimpleClient() throws UnknownHostException {
+        InetSocketAddress address = InetSocketAddress.createUnresolved("localhost", 5683);
+
+        Registration.Builder builder = new Registration.Builder("ID", "urn:client",
+                Identity.unsecure(Inet4Address.getLoopbackAddress(), 12354), address);
+
+        Registration reg = builder.build();
+        presenceService.setAwake(reg);
+        return reg;
+    }
+
+    private Registration givenASimpleClientWithQueueMode() throws UnknownHostException {
+        InetSocketAddress address = InetSocketAddress.createUnresolved("localhost", 5683);
+
+        Registration.Builder builder = new Registration.Builder("ID", "urn:client",
+                Identity.unsecure(Inet4Address.getLoopbackAddress(), 12354), address);
+
+        Registration reg = builder.bindingMode(BindingMode.UQ).build();
+        presenceService.setAwake(reg);
+        return reg;
+    }
+}
\ No newline at end of file