Repository: eclipse/leshan
Pull request #1036 (2021-06-30 08:43:55)

Add BootstrapReadRequest support.


Patch:
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ObjectResource.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ObjectResource.java
index f5cdd0fe..3ee089cc 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
@@ -48,6 +48,7 @@ import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
 import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
+import org.eclipse.leshan.core.request.BootstrapReadRequest;
 import org.eclipse.leshan.core.request.BootstrapWriteRequest;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.CreateRequest;
@@ -62,6 +63,7 @@ import org.eclipse.leshan.core.request.WriteRequest;
 import org.eclipse.leshan.core.request.WriteRequest.Mode;
 import org.eclipse.leshan.core.response.BootstrapDeleteResponse;
 import org.eclipse.leshan.core.response.BootstrapDiscoverResponse;
+import org.eclipse.leshan.core.response.BootstrapReadResponse;
 import org.eclipse.leshan.core.response.BootstrapWriteResponse;
 import org.eclipse.leshan.core.response.CreateResponse;
 import org.eclipse.leshan.core.response.DeleteResponse;
@@ -152,21 +154,41 @@ public class ObjectResource extends LwM2mClientCoapResource implements ObjectLis
                     return;
                 }
             }
-            // Manage Read Request
+
             else {
-                ReadRequest readRequest = new ReadRequest(requestedContentFormat, URI, coapRequest);
-                ReadResponse response = nodeEnabler.read(identity, readRequest);
-                if (response.getCode() == org.eclipse.leshan.core.ResponseCode.CONTENT) {
-                    LwM2mPath path = new LwM2mPath(URI);
-                    LwM2mNode content = response.getContent();
-                    LwM2mModel model = new StaticModel(nodeEnabler.getObjectModel());
-                    ContentFormat format = getContentFormat(readRequest, requestedContentFormat);
-                    exchange.respond(ResponseCode.CONTENT, encoder.encode(content, format, path, model),
-                            format.getCode());
-                    return;
+                if (identity.isLwm2mBootstrapServer()) {
+                    // Manage Bootstrap Read Request
+                    BootstrapReadRequest readRequest = new BootstrapReadRequest(requestedContentFormat, URI,
+                            coapRequest);
+                    BootstrapReadResponse response = nodeEnabler.read(identity, readRequest);
+                    if (response.getCode() == org.eclipse.leshan.core.ResponseCode.CONTENT) {
+                        LwM2mPath path = new LwM2mPath(URI);
+                        LwM2mNode content = response.getContent();
+                        LwM2mModel model = new StaticModel(nodeEnabler.getObjectModel());
+                        ContentFormat format = getContentFormat(readRequest, requestedContentFormat);
+                        exchange.respond(ResponseCode.CONTENT, encoder.encode(content, format, path, model),
+                                format.getCode());
+                        return;
+                    } else {
+                        exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage());
+                        return;
+                    }
                 } else {
-                    exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage());
-                    return;
+                    // Manage Read Request
+                    ReadRequest readRequest = new ReadRequest(requestedContentFormat, URI, coapRequest);
+                    ReadResponse response = nodeEnabler.read(identity, readRequest);
+                    if (response.getCode() == org.eclipse.leshan.core.ResponseCode.CONTENT) {
+                        LwM2mPath path = new LwM2mPath(URI);
+                        LwM2mNode content = response.getContent();
+                        LwM2mModel model = new StaticModel(nodeEnabler.getObjectModel());
+                        ContentFormat format = getContentFormat(readRequest, requestedContentFormat);
+                        exchange.respond(ResponseCode.CONTENT, encoder.encode(content, format, path, model),
+                                format.getCode());
+                        return;
+                    } else {
+                        exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage());
+                        return;
+                    }
                 }
             }
         }
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 dfb83ac2..96e303ef 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
@@ -40,6 +40,7 @@ import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
 import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
+import org.eclipse.leshan.core.request.BootstrapReadRequest;
 import org.eclipse.leshan.core.request.BootstrapWriteRequest;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.CreateRequest;
@@ -53,6 +54,7 @@ import org.eclipse.leshan.core.request.WriteAttributesRequest;
 import org.eclipse.leshan.core.request.WriteRequest;
 import org.eclipse.leshan.core.response.BootstrapDeleteResponse;
 import org.eclipse.leshan.core.response.BootstrapDiscoverResponse;
+import org.eclipse.leshan.core.response.BootstrapReadResponse;
 import org.eclipse.leshan.core.response.BootstrapWriteResponse;
 import org.eclipse.leshan.core.response.CreateResponse;
 import org.eclipse.leshan.core.response.DeleteResponse;
@@ -180,6 +182,29 @@ public abstract class BaseObjectEnabler implements LwM2mObjectEnabler {
         return ReadResponse.internalServerError("not implemented");
     }
 
+    @Override
+    public BootstrapReadResponse read(ServerIdentity identity, BootstrapReadRequest request) {
+        // read is not supported for bootstrap
+        if (identity.isLwm2mServer()) {
+            return BootstrapReadResponse.methodNotAllowed();
+        }
+
+        if (!identity.isSystem()) {
+            LwM2mPath path = request.getPath();
+
+            // BootstrapRead can only target object 1 and 2
+            if (path.getObjectId() != 1 && path.getObjectId() != 2) {
+                return BootstrapReadResponse.badRequest("bootstrap read can only target Object 1 (Server) or 2 (ACL)");
+            }
+        }
+        return doRead(identity, request);
+    }
+
+    protected BootstrapReadResponse doRead(ServerIdentity identity, BootstrapReadRequest request) {
+        // This should be a not implemented error, but this is not defined in the spec.
+        return BootstrapReadResponse.internalServerError("not implemented");
+    }
+
     @Override
     public synchronized WriteResponse write(ServerIdentity identity, WriteRequest request) {
         try {
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectEnabler.java
index 6b31e748..ea882db4 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectEnabler.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectEnabler.java
@@ -27,6 +27,7 @@ import org.eclipse.leshan.core.Stoppable;
 import org.eclipse.leshan.core.model.ObjectModel;
 import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
 import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
+import org.eclipse.leshan.core.request.BootstrapReadRequest;
 import org.eclipse.leshan.core.request.BootstrapWriteRequest;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.CreateRequest;
@@ -40,6 +41,7 @@ import org.eclipse.leshan.core.request.WriteAttributesRequest;
 import org.eclipse.leshan.core.request.WriteRequest;
 import org.eclipse.leshan.core.response.BootstrapDeleteResponse;
 import org.eclipse.leshan.core.response.BootstrapDiscoverResponse;
+import org.eclipse.leshan.core.response.BootstrapReadResponse;
 import org.eclipse.leshan.core.response.BootstrapWriteResponse;
 import org.eclipse.leshan.core.response.CreateResponse;
 import org.eclipse.leshan.core.response.DeleteResponse;
@@ -79,6 +81,8 @@ public interface LwM2mObjectEnabler {
 
     ReadResponse read(ServerIdentity identity, ReadRequest request);
 
+    BootstrapReadResponse read(ServerIdentity identity, BootstrapReadRequest request);
+
     WriteResponse write(ServerIdentity identity, WriteRequest request);
 
     BootstrapWriteResponse write(ServerIdentity identity, BootstrapWriteRequest request);
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 69ed4e2b..2de0be13 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
@@ -39,6 +39,7 @@ import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
+import org.eclipse.leshan.core.request.BootstrapReadRequest;
 import org.eclipse.leshan.core.request.BootstrapWriteRequest;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.CreateRequest;
@@ -50,6 +51,7 @@ 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.response.BootstrapDeleteResponse;
+import org.eclipse.leshan.core.response.BootstrapReadResponse;
 import org.eclipse.leshan.core.response.BootstrapWriteResponse;
 import org.eclipse.leshan.core.response.CreateResponse;
 import org.eclipse.leshan.core.response.DeleteResponse;
@@ -224,6 +226,14 @@ public class ObjectEnabler extends BaseObjectEnabler implements Destroyable, Sta
         return instance.read(identity, path.getResourceId(), path.getResourceInstanceId());
     }
 
+    @Override
+    protected BootstrapReadResponse doRead(ServerIdentity identity, BootstrapReadRequest request) {
+        // Basic implementation we delegate to classic Read Request
+        ReadResponse response = doRead(identity,
+                new ReadRequest(request.getContentFormat(), request.getPath(), request.getCoapRequest()));
+        return new BootstrapReadResponse(response.getCode(), response.getContent(), response.getErrorMessage());
+    }
+
     @Override
     protected ObserveResponse doObserve(final ServerIdentity identity, final ObserveRequest request) {
         final LwM2mPath path = request.getPath();
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/BootstrapReadRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/BootstrapReadRequest.java
new file mode 100644
index 00000000..efa01fa2
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/BootstrapReadRequest.java
@@ -0,0 +1,165 @@
+/*******************************************************************************
+ * Copyright (c) 2013-2015 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.request;
+
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.request.exception.InvalidRequestException;
+import org.eclipse.leshan.core.response.BootstrapReadResponse;
+
+/**
+ * A Lightweight M2M request for retrieving the values of resources from a LWM2M Client.
+ * 
+ * The "Bootstrap-Read" operation in the Bootstrap Interface is a restricted form of the "Read" operation found in the
+ * Device Management and Service Enablement interface, and MUST be limited to target Objects that are strictly necessary
+ * to setup a proper configuration in a LwM2M Client.
+ */
+public class BootstrapReadRequest extends AbstractSimpleDownlinkRequest<BootstrapReadResponse>
+        implements BootstrapDownlinkRequest<BootstrapReadResponse> {
+
+    private final ContentFormat format;
+
+    /**
+     * Creates a request for reading all instances of a particular object from a client.
+     * 
+     * @param objectId the object ID of the resource
+     */
+    public BootstrapReadRequest(int objectId) {
+        this(null, new LwM2mPath(objectId), null);
+    }
+
+    /**
+     * Creates a request for reading all instances of a particular object from a client.
+     * 
+     * @param format the desired format for the response
+     * @param objectId the object ID of the resource
+     */
+    public BootstrapReadRequest(ContentFormat format, int objectId) {
+        this(format, new LwM2mPath(objectId), null);
+    }
+
+    /**
+     * Creates a request for reading a particular object instance from a client.
+     * 
+     * @param objectId the object ID of the resource
+     * @param objectInstanceId the object instance ID
+     */
+    public BootstrapReadRequest(int objectId, int objectInstanceId) {
+        this(null, new LwM2mPath(objectId, objectInstanceId), null);
+    }
+
+    /**
+     * Creates a request for reading a particular object instance from a client.
+     *
+     * @param format the desired format for the response
+     * @param objectId the object ID of the resource
+     * @param objectInstanceId the object instance ID
+     */
+    public BootstrapReadRequest(ContentFormat format, int objectId, int objectInstanceId) {
+        this(format, new LwM2mPath(objectId, objectInstanceId), null);
+    }
+
+    /**
+     * Create a request for reading an object/instance targeted by a specific path.
+     * 
+     * @param path the path to the LWM2M node to read
+     * @throws IllegalArgumentException if the target path is not valid
+     */
+    public BootstrapReadRequest(String path) {
+        this(null, new LwM2mPath(path), null);
+    }
+
+    /**
+     * Create a request for reading an object/instance targeted by a specific path.
+     *
+     * @param format the desired format for the response
+     * @param path the path to the LWM2M node to read
+     * @throws IllegalArgumentException if the target path is not valid
+     */
+    public BootstrapReadRequest(ContentFormat format, String path) {
+        this(format, new LwM2mPath(path), null);
+    }
+
+    /**
+     * Create a request for reading an object/instance targeted by a specific path.
+     *
+     * @param format the desired format for the response
+     * @param path the path to the LWM2M node to read
+     * @param coapRequest the underlying request
+     * 
+     * @throws IllegalArgumentException if the target path is not valid
+     */
+    public BootstrapReadRequest(ContentFormat format, String path, Object coapRequest) {
+        this(format, new LwM2mPath(path), coapRequest);
+    }
+
+    /**
+     * Create a request for reading an object/instance targeted by a specific path.
+     * <p>
+     * This constructor is mainly for internal purpose.
+     * 
+     * @param format the desired format for the response
+     * @param target the path to the LWM2M node to read
+     * @param coapRequest the underlying request
+     * 
+     * @throws IllegalArgumentException if the target path is not valid
+     */
+    public BootstrapReadRequest(ContentFormat format, LwM2mPath target, Object coapRequest) {
+        super(target, coapRequest);
+        if (!target.isObject() && !target.isObjectInstance())
+            throw new InvalidRequestException("Bootstrap Read request cannot only target Object and Object instance");
+        this.format = format;
+    }
+
+    /**
+     * @return the desired format of the resource to read
+     */
+    public ContentFormat getContentFormat() {
+        return format;
+    }
+
+    @Override
+    public final String toString() {
+        return String.format("BootstrapReadRequest [path=%s format=%s]", getPath(), format);
+    }
+
+    @Override
+    public void accept(DownlinkRequestVisitor visitor) {
+        visitor.visit(this);
+
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((format == null) ? 0 : format.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        BootstrapReadRequest other = (BootstrapReadRequest) obj;
+        if (format != other.format)
+            return false;
+        return true;
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownLinkRequestVisitorAdapter.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownLinkRequestVisitorAdapter.java
index 26abd6da..30437689 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownLinkRequestVisitorAdapter.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownLinkRequestVisitorAdapter.java
@@ -73,6 +73,10 @@ public class DownLinkRequestVisitorAdapter implements DownlinkRequestVisitor {
     public void visit(BootstrapWriteRequest request) {
     }
 
+    @Override
+    public void visit(BootstrapReadRequest request) {
+    }
+
     @Override
     public void visit(BootstrapDeleteRequest request) {
     }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownlinkRequestVisitor.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownlinkRequestVisitor.java
index 92a43765..d1fb964a 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownlinkRequestVisitor.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownlinkRequestVisitor.java
@@ -45,6 +45,8 @@ public interface DownlinkRequestVisitor {
 
     void visit(BootstrapWriteRequest request);
 
+    void visit(BootstrapReadRequest request);
+
     void visit(BootstrapDeleteRequest request);
 
     void visit(BootstrapFinishRequest request);
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/response/BootstrapReadResponse.java b/leshan-core/src/main/java/org/eclipse/leshan/core/response/BootstrapReadResponse.java
new file mode 100644
index 00000000..a9a2a2d5
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/response/BootstrapReadResponse.java
@@ -0,0 +1,107 @@
+/*******************************************************************************
+ * Copyright (c) 2013-2015 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.response;
+
+import org.eclipse.leshan.core.ResponseCode;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.request.exception.InvalidResponseException;
+
+public class BootstrapReadResponse extends AbstractLwM2mResponse {
+
+    protected final LwM2mNode content;
+
+    public BootstrapReadResponse(ResponseCode code, LwM2mNode content, String errorMessage) {
+        this(code, content, errorMessage, null);
+    }
+
+    public BootstrapReadResponse(ResponseCode code, LwM2mNode content, String errorMessage, Object coapResponse) {
+        super(code, errorMessage, coapResponse);
+
+        if (ResponseCode.CONTENT.equals(code)) {
+            if (content == null)
+                throw new InvalidResponseException("Content is mandatory for successful response");
+        }
+        this.content = content;
+    }
+
+    @Override
+    public boolean isSuccess() {
+        return getCode() == ResponseCode.CONTENT;
+    }
+
+    @Override
+    public boolean isValid() {
+        switch (code.getCode()) {
+        case ResponseCode.CONTENT_CODE:
+        case ResponseCode.BAD_REQUEST_CODE:
+        case ResponseCode.UNAUTHORIZED_CODE:
+        case ResponseCode.NOT_FOUND_CODE:
+        case ResponseCode.METHOD_NOT_ALLOWED_CODE:
+        case ResponseCode.NOT_ACCEPTABLE_CODE:
+        case ResponseCode.INTERNAL_SERVER_ERROR_CODE:
+            return true;
+        default:
+            return false;
+        }
+    }
+
+    /**
+     * Get the {@link LwM2mNode} value returned as response payload.
+     *
+     * @return the value or <code>null</code> if the client returned an error response.
+     */
+    public LwM2mNode getContent() {
+        return content;
+    }
+
+    @Override
+    public String toString() {
+        if (errorMessage != null)
+            return String.format("BootstrapReadResponse [code=%s, errormessage=%s]", code, errorMessage);
+        else
+            return String.format("BootstrapReadResponse [code=%s, content=%s]", code, content);
+    }
+
+    // Syntactic sugar static constructors :
+
+    public static BootstrapReadResponse success(LwM2mNode content) {
+        return new BootstrapReadResponse(ResponseCode.CONTENT, content, null, null);
+    }
+
+    public static BootstrapReadResponse notFound() {
+        return new BootstrapReadResponse(ResponseCode.NOT_FOUND, null, null);
+    }
+
+    public static BootstrapReadResponse unauthorized() {
+        return new BootstrapReadResponse(ResponseCode.UNAUTHORIZED, null, null);
+    }
+
+    public static BootstrapReadResponse methodNotAllowed() {
+        return new BootstrapReadResponse(ResponseCode.METHOD_NOT_ALLOWED, null, null);
+    }
+
+    public static BootstrapReadResponse notAcceptable() {
+        return new BootstrapReadResponse(ResponseCode.NOT_ACCEPTABLE, null, null);
+    }
+
+    public static BootstrapReadResponse badRequest(String errorMessage) {
+        return new BootstrapReadResponse(ResponseCode.BAD_REQUEST, null, errorMessage);
+    }
+
+    public static BootstrapReadResponse internalServerError(String errorMessage) {
+        return new BootstrapReadResponse(ResponseCode.INTERNAL_SERVER_ERROR, null, errorMessage);
+    }
+}
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java
index 5a31fada..8c5afb19 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java
@@ -31,6 +31,7 @@ import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
 import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
 import org.eclipse.leshan.core.request.BootstrapFinishRequest;
+import org.eclipse.leshan.core.request.BootstrapReadRequest;
 import org.eclipse.leshan.core.request.BootstrapWriteRequest;
 import org.eclipse.leshan.core.request.CancelObservationRequest;
 import org.eclipse.leshan.core.request.ContentFormat;
@@ -211,6 +212,15 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor {
         applyLowerLayerConfig(coapRequest);
     }
 
+    @Override
+    public void visit(BootstrapReadRequest request) {
+        coapRequest = Request.newGet();
+        if (request.getContentFormat() != null)
+            coapRequest.getOptions().setAccept(request.getContentFormat().getCode());
+        setTarget(coapRequest, request.getPath());
+        applyLowerLayerConfig(coapRequest);
+    }
+
     @Override
     public void visit(BootstrapDiscoverRequest request) {
         coapRequest = Request.newGet();
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java
index d6f2032b..b69fb804 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java
@@ -34,6 +34,7 @@ import org.eclipse.leshan.core.observation.Observation;
 import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
 import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
 import org.eclipse.leshan.core.request.BootstrapFinishRequest;
+import org.eclipse.leshan.core.request.BootstrapReadRequest;
 import org.eclipse.leshan.core.request.BootstrapWriteRequest;
 import org.eclipse.leshan.core.request.CancelObservationRequest;
 import org.eclipse.leshan.core.request.ContentFormat;
@@ -53,6 +54,7 @@ import org.eclipse.leshan.core.request.exception.InvalidResponseException;
 import org.eclipse.leshan.core.response.BootstrapDeleteResponse;
 import org.eclipse.leshan.core.response.BootstrapDiscoverResponse;
 import org.eclipse.leshan.core.response.BootstrapFinishResponse;
+import org.eclipse.leshan.core.response.BootstrapReadResponse;
 import org.eclipse.leshan.core.response.BootstrapWriteResponse;
 import org.eclipse.leshan.core.response.CancelObservationResponse;
 import org.eclipse.leshan.core.response.CreateResponse;
@@ -329,6 +331,22 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
         }
     }
 
+    @Override
+    public void visit(BootstrapReadRequest request) {
+        if (coapResponse.isError()) {
+            // handle error response:
+            lwM2mresponse = new BootstrapReadResponse(toLwM2mResponseCode(coapResponse.getCode()), null,
+                    coapResponse.getPayloadString(), coapResponse);
+        } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT) {
+            // handle success response:
+            LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint);
+            lwM2mresponse = new BootstrapReadResponse(ResponseCode.CONTENT, content, null, coapResponse);
+        } else {
+            // handle unexpected response:
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
+        }
+    }
+
     @Override
     public void visit(BootstrapDeleteRequest request) {
         if (coapResponse.isError()) {


Test patch:
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 a0e8956d..b920b107 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
@@ -34,11 +34,14 @@ import org.eclipse.leshan.core.SecurityMode;
 import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
+import org.eclipse.leshan.core.request.BootstrapReadRequest;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.ExecuteRequest;
 import org.eclipse.leshan.core.request.ReadRequest;
 import org.eclipse.leshan.core.request.exception.InvalidRequestException;
 import org.eclipse.leshan.core.request.exception.RequestCanceledException;
+import org.eclipse.leshan.core.response.BootstrapDiscoverResponse;
+import org.eclipse.leshan.core.response.BootstrapReadResponse;
 import org.eclipse.leshan.core.response.ExecuteResponse;
 import org.eclipse.leshan.core.response.ReadResponse;
 import org.eclipse.leshan.integration.tests.util.BootstrapIntegrationTestHelper;
@@ -222,13 +225,15 @@ public class BootstrapTest {
 
         // check the client is registered
         helper.assertClientRegisterered();
-        assertNotNull(helper.lastDiscoverAnswer);
-        assertEquals(ResponseCode.CONTENT, helper.lastDiscoverAnswer.getCode());
+        assertNotNull(helper.lastCustomResponse);
+        assertTrue(helper.lastCustomResponse instanceof BootstrapDiscoverResponse);
+        BootstrapDiscoverResponse lastDiscoverAnswer = (BootstrapDiscoverResponse) helper.lastCustomResponse;
+        assertEquals(ResponseCode.CONTENT, lastDiscoverAnswer.getCode());
         assertEquals(
                 String.format("</>;lwm2m=1.0,</0>;ver=1.1,</0/0>;uri=\"coap://%s:%d\",</1>;ver=1.1,</3>;ver=1.1,</3/0>",
                         helper.bootstrapServer.getUnsecuredAddress().getHostString(),
                         helper.bootstrapServer.getUnsecuredAddress().getPort()),
-                Link.serialize(helper.lastDiscoverAnswer.getObjectLinks()));
+                Link.serialize(lastDiscoverAnswer.getObjectLinks()));
     }
 
     @Test
@@ -252,13 +257,14 @@ public class BootstrapTest {
 
         // check the client is registered
         helper.assertClientRegisterered();
-        assertNotNull(helper.lastDiscoverAnswer);
-        assertEquals(ResponseCode.CONTENT, helper.lastDiscoverAnswer.getCode());
+        assertTrue(helper.lastCustomResponse instanceof BootstrapDiscoverResponse);
+        BootstrapDiscoverResponse lastDiscoverAnswer = (BootstrapDiscoverResponse) helper.lastCustomResponse;
+        assertEquals(ResponseCode.CONTENT, lastDiscoverAnswer.getCode());
         assertEquals(
                 String.format("</>;lwm2m=1.0,</0>;ver=1.1,</0/0>;uri=\"coap://%s:%d\",</1>;ver=1.1,</3>;ver=1.1,</3/0>",
                         helper.bootstrapServer.getUnsecuredAddress().getHostString(),
                         helper.bootstrapServer.getUnsecuredAddress().getPort()),
-                Link.serialize(helper.lastDiscoverAnswer.getObjectLinks()));
+                Link.serialize(lastDiscoverAnswer.getObjectLinks()));
 
         // re-bootstrap
         try {
@@ -274,14 +280,15 @@ public class BootstrapTest {
         helper.waitForBootstrapFinishedAtClientSide(1);
 
         // check last discover response
-        assertNotNull(helper.lastDiscoverAnswer);
-        assertEquals(ResponseCode.CONTENT, helper.lastDiscoverAnswer.getCode());
+        assertTrue(helper.lastCustomResponse instanceof BootstrapDiscoverResponse);
+        lastDiscoverAnswer = (BootstrapDiscoverResponse) helper.lastCustomResponse;
+        assertEquals(ResponseCode.CONTENT, lastDiscoverAnswer.getCode());
         assertEquals(String.format(
                 "</>;lwm2m=1.0,</0>;ver=1.1,</0/0>;uri=\"coap://%s:%d\",</0/1>;ssid=2222;uri=\"coap://%s:%d\",</1>;ver=1.1,</1/0>;ssid=2222,</3>;ver=1.1,</3/0>",
                 helper.bootstrapServer.getUnsecuredAddress().getHostString(),
                 helper.bootstrapServer.getUnsecuredAddress().getPort(),
                 helper.server.getUnsecuredAddress().getHostString(), helper.server.getUnsecuredAddress().getPort()),
-                Link.serialize(helper.lastDiscoverAnswer.getObjectLinks()));
+                Link.serialize(lastDiscoverAnswer.getObjectLinks()));
 
     }
 
@@ -305,9 +312,55 @@ public class BootstrapTest {
 
         // check the client is registered
         helper.assertClientRegisterered();
-        assertNotNull(helper.lastDiscoverAnswer);
-        assertEquals(ResponseCode.CONTENT, helper.lastDiscoverAnswer.getCode());
-        assertEquals("</>;lwm2m=1.0,</3>;ver=1.1,</3/0>", Link.serialize(helper.lastDiscoverAnswer.getObjectLinks()));
+        assertTrue(helper.lastCustomResponse instanceof BootstrapDiscoverResponse);
+        BootstrapDiscoverResponse lastDiscoverAnswer = (BootstrapDiscoverResponse) helper.lastCustomResponse;
+        assertEquals(ResponseCode.CONTENT, lastDiscoverAnswer.getCode());
+        assertEquals("</>;lwm2m=1.0,</3>;ver=1.1,</3/0>", Link.serialize(lastDiscoverAnswer.getObjectLinks()));
+    }
+
+    @Test
+    public void bootstrapWithReadOnServerThenRebootstrap() throws InvalidRequestException, InterruptedException {
+        // Create DM Server without security & start it
+        helper.createServer();
+        helper.server.start();
+
+        // Create and start bootstrap server
+        helper.createBootstrapServer(null, null, new BootstrapReadRequest(1));
+        helper.bootstrapServer.start();
+
+        // Create Client and check it is not already registered
+        helper.createClient();
+        helper.assertClientNotRegisterered();
+
+        // Start it and wait for registration
+        helper.client.start();
+        helper.waitForBootstrapFinishedAtClientSide(1);
+        helper.waitForRegistrationAtServerSide(1);
+
+        // check the client is registered
+        helper.assertClientRegisterered();
+        assertTrue(helper.lastCustomResponse instanceof BootstrapReadResponse);
+        BootstrapReadResponse lastReadResponse = (BootstrapReadResponse) helper.lastCustomResponse;
+        assertEquals(ResponseCode.CONTENT, lastReadResponse.getCode());
+        assertEquals(new LwM2mObject(1), lastReadResponse.getContent());
+
+        // re-bootstrap
+        try {
+            ExecuteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                    new ExecuteRequest("/1/0/9"));
+            assertTrue(response.isSuccess());
+        } catch (RequestCanceledException e) {
+            // request can be cancelled if server does not received the execute response before the de-registration
+            // so we just ignore this error.
+        }
+
+        // wait bootstrap finished
+        helper.waitForBootstrapFinishedAtClientSide(5);
+
+        // check last discover response
+        assertTrue(helper.lastCustomResponse instanceof BootstrapReadResponse);
+        lastReadResponse = (BootstrapReadResponse) helper.lastCustomResponse;
+        assertEquals(ResponseCode.CONTENT, lastReadResponse.getCode());
     }
 
     @Test
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapIntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapIntegrationTestHelper.java
index 5eaacfb5..5fb48465 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapIntegrationTestHelper.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapIntegrationTestHelper.java
@@ -54,11 +54,10 @@ import org.eclipse.leshan.core.LwM2mId;
 import org.eclipse.leshan.core.SecurityMode;
 import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
 import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
-import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
+import org.eclipse.leshan.core.request.BootstrapDownlinkRequest;
 import org.eclipse.leshan.core.request.BootstrapRequest;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.Identity;
-import org.eclipse.leshan.core.response.BootstrapDiscoverResponse;
 import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.core.util.Hex;
 import org.eclipse.leshan.server.bootstrap.BootstrapConfig;
@@ -89,7 +88,7 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
     public LeshanBootstrapServer bootstrapServer;
     public final PublicKey bootstrapServerPublicKey;
     public final PrivateKey bootstrapServerPrivateKey;
-    public volatile BootstrapDiscoverResponse lastDiscoverAnswer;
+    public volatile LwM2mResponse lastCustomResponse;
 
     private SynchronousBootstrapListener bootstrapListener = new SynchronousBootstrapListener();
 
@@ -169,7 +168,7 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
     }
 
     public void createBootstrapServer(BootstrapSecurityStore securityStore, BootstrapConfigStore bootstrapStore,
-            final BootstrapDiscoverRequest request) {
+            final BootstrapDownlinkRequest<?> firstCustomRequest) {
         LeshanBootstrapServerBuilder builder = createBootstrapBuilder(securityStore, bootstrapStore);
         if (bootstrapStore == null) {
             bootstrapStore = unsecuredBootstrapStore();
@@ -181,14 +180,18 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
         BootstrapConfigStoreTaskProvider taskProvider = new BootstrapConfigStoreTaskProvider(bootstrapStore) {
             @Override
             public Tasks getTasks(BootstrapSession session, List<LwM2mResponse> previousResponses) {
-                if (previousResponses == null) {
+               if (previousResponses == null) {
                     Tasks tasks = new Tasks();
-                    tasks.requestsToSend = new ArrayList<>();
-                    tasks.requestsToSend.add(request);
+                    tasks.requestsToSend = new ArrayList<>(1);
+                    tasks.requestsToSend.add(firstCustomRequest);
                     tasks.last = false;
+                    tasks.supportedObjects = new HashMap<>();
+                    tasks.supportedObjects.put(0, "1.1");
+                    tasks.supportedObjects.put(1, "1.1");
+                    tasks.supportedObjects.put(2, "1.0");
                     return tasks;
                 } else {
-                    lastDiscoverAnswer = (BootstrapDiscoverResponse) previousResponses.get(0);
+                    lastCustomResponse = previousResponses.get(0);
                     return super.getTasks(session, null);
                 }
             }
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/TestObjectsInitializer.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/TestObjectsInitializer.java
index 27d6e6af..27afd722 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/TestObjectsInitializer.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/TestObjectsInitializer.java
@@ -30,6 +30,7 @@ import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectModel;
 import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
 import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
+import org.eclipse.leshan.core.request.BootstrapReadRequest;
 import org.eclipse.leshan.core.request.BootstrapWriteRequest;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.CreateRequest;
@@ -43,6 +44,7 @@ import org.eclipse.leshan.core.request.WriteAttributesRequest;
 import org.eclipse.leshan.core.request.WriteRequest;
 import org.eclipse.leshan.core.response.BootstrapDeleteResponse;
 import org.eclipse.leshan.core.response.BootstrapDiscoverResponse;
+import org.eclipse.leshan.core.response.BootstrapReadResponse;
 import org.eclipse.leshan.core.response.BootstrapWriteResponse;
 import org.eclipse.leshan.core.response.CreateResponse;
 import org.eclipse.leshan.core.response.DeleteResponse;
@@ -106,6 +108,13 @@ public class TestObjectsInitializer extends ObjectsInitializer {
                 return nodeEnabler.read(identity, request);
             }
 
+            @Override
+            public BootstrapReadResponse read(ServerIdentity identity, BootstrapReadRequest request) {
+                if (!identity.isSystem())
+                    assertThat(request.getCoapRequest(), instanceOf(Request.class));
+                return nodeEnabler.read(identity, request);
+            }
+
             @Override
             public ObserveResponse observe(ServerIdentity identity, ObserveRequest request) {
                 if (!identity.isSystem())