Repository: eclipse/leshan
Pull request #949 (2020-12-15 16:56:57)

Add preferred Content format to BootstrapRequest.
(see http://www.openmobilealliance.org/release/LightweightM2M/V1_1_1-20190617-A/HTML-Version/OMA-TS-LightweightM2M_Core-V1_1_1-20190617-A.html#6-1-7-1-0-6171-Bootstrap-Request-Operation)


Patch:
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 46ce6bdf..8d661a1a 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
@@ -57,6 +57,9 @@ public class CoapRequestBuilder implements UplinkRequestVisitor {
         HashMap<String, String> attributes = new HashMap<>();
         attributes.putAll(request.getAdditionalAttributes());
         attributes.put("ep", request.getEndpointName());
+        if (request.getPreferredContentFormat() != null) {
+            attributes.put("pct", Integer.toString(request.getPreferredContentFormat().getCode()));
+        }
         for (Entry<String, String> attr : attributes.entrySet()) {
             coapRequest.getOptions().addUriQuery(attr.getKey() + "=" + attr.getValue());
         }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java
index b039ef5f..8795410f 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java
@@ -46,6 +46,7 @@ import org.eclipse.leshan.core.LwM2mId;
 import org.eclipse.leshan.core.ResponseCode;
 import org.eclipse.leshan.core.request.BindingMode;
 import org.eclipse.leshan.core.request.BootstrapRequest;
+import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.DeregisterRequest;
 import org.eclipse.leshan.core.request.Identity;
 import org.eclipse.leshan.core.request.RegisterRequest;
@@ -100,6 +101,7 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
 
     // device state
     private final String endpoint;
+    private final ContentFormat preferredContentFormat; // used for bootstrap
     private final Map<String, String> additionalAttributes;
     private final Map<String, String> bsAdditionalAttributes; // @since 1.1
     private final Map<Integer /* objectId */, LwM2mObjectEnabler> objectEnablers;
@@ -129,7 +131,7 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
             Integer communicationPeriodInMs, boolean reconnectOnUpdate, boolean resumeOnConnect) {
         this(endpoint, objectTree, endpointsManager, requestSender, bootstrapState, observer, additionalAttributes,
                 null, executor, requestTimeoutInMs, deregistrationTimeoutInMs, bootstrapSessionTimeoutInSec,
-                retryWaitingTimeInMs, communicationPeriodInMs, reconnectOnUpdate, resumeOnConnect, false);
+                retryWaitingTimeInMs, communicationPeriodInMs, reconnectOnUpdate, resumeOnConnect, false, null);
     }
 
     /** @since 1.1 */
@@ -138,7 +140,8 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
             Map<String, String> additionalAttributes, Map<String, String> bsAdditionalAttributes,
             ScheduledExecutorService executor, long requestTimeoutInMs, long deregistrationTimeoutInMs,
             int bootstrapSessionTimeoutInSec, int retryWaitingTimeInMs, Integer communicationPeriodInMs,
-            boolean reconnectOnUpdate, boolean resumeOnConnect, boolean useQueueMode) {
+            boolean reconnectOnUpdate, boolean resumeOnConnect, boolean useQueueMode,
+            ContentFormat preferredContentFormat) {
         this.endpoint = endpoint;
         this.objectEnablers = objectTree.getObjectEnablers();
         this.bootstrapHandler = bootstrapState;
@@ -157,6 +160,7 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
         this.reconnectOnUpdate = reconnectOnUpdate;
         this.resumeOnConnect = resumeOnConnect;
         this.queueMode = useQueueMode;
+        this.preferredContentFormat = preferredContentFormat;
 
         if (executor == null) {
             schedExecutor = createScheduledExecutor();
@@ -223,7 +227,7 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
             // Send bootstrap request
             BootstrapRequest request = null;
             try {
-                request = new BootstrapRequest(endpoint, bsAdditionalAttributes);
+                request = new BootstrapRequest(endpoint, preferredContentFormat, bsAdditionalAttributes);
                 if (observer != null) {
                     observer.onBootstrapStarted(bootstrapServer, request);
                 }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngineFactory.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngineFactory.java
index 5bc84833..547470d6 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngineFactory.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngineFactory.java
@@ -23,6 +23,7 @@ import org.eclipse.leshan.client.bootstrap.BootstrapHandler;
 import org.eclipse.leshan.client.observer.LwM2mClientObserver;
 import org.eclipse.leshan.client.request.LwM2mRequestSender;
 import org.eclipse.leshan.client.resource.LwM2mObjectTree;
+import org.eclipse.leshan.core.request.ContentFormat;
 
 /**
  * A default implementation of {@link RegistrationEngineFactory}.
@@ -39,6 +40,7 @@ public class DefaultRegistrationEngineFactory implements RegistrationEngineFacto
     private boolean reconnectOnUpdate = false;
     private boolean resumeOnConnect = true;
     private boolean queueMode = false;
+    private ContentFormat preferredContentFormat = ContentFormat.SENML_CBOR;
 
     public DefaultRegistrationEngineFactory() {
     }
@@ -51,7 +53,7 @@ public class DefaultRegistrationEngineFactory implements RegistrationEngineFacto
         return new DefaultRegistrationEngine(endpoint, objectTree, endpointsManager, requestSender, bootstrapState,
                 observer, additionalAttributes, bsAdditionalAttributes, sharedExecutor, requestTimeoutInMs,
                 deregistrationTimeoutInMs, bootstrapSessionTimeoutInSec, retryWaitingTimeInMs, communicationPeriodInMs,
-                reconnectOnUpdate, resumeOnConnect, queueMode);
+                reconnectOnUpdate, resumeOnConnect, queueMode, preferredContentFormat);
     }
 
     /**
@@ -164,4 +166,45 @@ public class DefaultRegistrationEngineFactory implements RegistrationEngineFacto
         this.queueMode = enable;
         return this;
     }
+
+    /**
+     * Define preferred content format for bootstrap session.
+     * <p>
+     * <code>null</code> can be used for "no preferred content format" but using a preferred content format could give a
+     * hint to Bootstrap server to know that your client is a LWM2M v1.1 client as this preferredContentFormat does not
+     * exist in LWM2M v1.0
+     * <p>
+     * Default value is {@link ContentFormat#SENML_CBOR}.
+     * 
+     * @param format preferred content format to use during bootstrap session. It must be SenML JSON, SenML CBOR, or TLV
+     *        or <code>null</code> if no preferred content format.
+     * @return this for fluent API
+     * 
+     * @see <a href=
+     *      "http://www.openmobilealliance.org/release/LightweightM2M/V1_1_1-20190617-A/HTML-Version/OMA-TS-LightweightM2M_Core-V1_1_1-20190617-A.html#6-1-7-1-0-6171-Bootstrap-Request-Operation">6.1.7.1.
+     *      Bootstrap-Request Operation</a>
+     * 
+     * @throws IllegalArgumentException if content format is not a valid one.
+     */
+    public DefaultRegistrationEngineFactory setPreferredContentFormat(ContentFormat format)
+            throws IllegalArgumentException {
+        // handle null case
+        if (format == null) {
+            this.preferredContentFormat = format;
+            return this;
+        }
+
+        // check allowed value
+        switch (format.getCode()) {
+        case ContentFormat.TLV_CODE:
+        case ContentFormat.SENML_CBOR_CODE:
+        case ContentFormat.SENML_JSON_CODE:
+        case ContentFormat.OLD_TLV_CODE:
+            this.preferredContentFormat = format;
+            return this;
+        default:
+            throw new IllegalArgumentException(String
+                    .format("Invalid preferred content format %s, it MUST be SenML JSON, SenML CBOR, or TLV", format));
+        }
+    }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/BootstrapRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/BootstrapRequest.java
index 0ce732db..d450b495 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/BootstrapRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/BootstrapRequest.java
@@ -24,24 +24,29 @@ import org.eclipse.leshan.core.response.BootstrapResponse;
 
 /**
  * The request to send to start a bootstrap session
+ * 
+ * @see <a href=
+ *      "http://www.openmobilealliance.org/release/LightweightM2M/V1_1_1-20190617-A/HTML-Version/OMA-TS-LightweightM2M_Core-V1_1_1-20190617-A.html#6-1-7-1-0-6171-Bootstrap-Request-Operation">6.1.7.1.
+ *      Bootstrap-Request Operation</a>
  */
 public class BootstrapRequest extends AbstractLwM2mRequest<BootstrapResponse>
         implements UplinkRequest<BootstrapResponse> {
 
     private final String endpointName;
     private final Map<String, String> additionalAttributes;
+    private final ContentFormat preferredContentFormat;
 
     public BootstrapRequest(String endpointName) throws InvalidRequestException {
-        this(endpointName, null);
+        this(endpointName, null, null);
     }
 
-    public BootstrapRequest(String endpointName, Map<String, String> additionalAttributes)
-            throws InvalidRequestException {
-        this(endpointName, additionalAttributes, null);
+    public BootstrapRequest(String endpointName, ContentFormat preferredFormat,
+            Map<String, String> additionalAttributes) throws InvalidRequestException {
+        this(endpointName, preferredFormat, additionalAttributes, null);
     }
 
-    public BootstrapRequest(String endpointName, Map<String, String> additionalAttributes, Object coapRequest)
-            throws InvalidRequestException {
+    public BootstrapRequest(String endpointName, ContentFormat preferredFormat,
+            Map<String, String> additionalAttributes, Object coapRequest) throws InvalidRequestException {
         super(coapRequest);
         if (endpointName == null || endpointName.isEmpty())
             throw new InvalidRequestException("endpoint is mandatory");
@@ -51,6 +56,25 @@ public class BootstrapRequest extends AbstractLwM2mRequest<BootstrapResponse>
             this.additionalAttributes = Collections.emptyMap();
         else
             this.additionalAttributes = Collections.unmodifiableMap(new HashMap<>(additionalAttributes));
+
+        // handle preferred format
+        if (preferredFormat == null) {
+            this.preferredContentFormat = null;
+        } else {
+            // The Content Format MUST be one of the SenML JSON, SenML CBOR, or TLV formats.
+            switch (preferredFormat.getCode()) {
+            case ContentFormat.TLV_CODE:
+            case ContentFormat.SENML_CBOR_CODE:
+            case ContentFormat.SENML_JSON_CODE:
+            case ContentFormat.OLD_TLV_CODE:
+                this.preferredContentFormat = preferredFormat;
+                break;
+            default:
+                throw new InvalidRequestException(
+                        "Invalid preferred content format %s, it MUST be SenML JSON, SenML CBOR, or TLV",
+                        preferredFormat);
+            }
+        }
     }
 
     public String getEndpointName() {
@@ -62,6 +86,14 @@ public class BootstrapRequest extends AbstractLwM2mRequest<BootstrapResponse>
         return additionalAttributes;
     }
 
+    /**
+     * The preferred Content Format for bootstrap configuration. The Content Format MUST be one of the SenML JSON, SenML
+     * CBOR, or TLV formats.
+     */
+    public ContentFormat getPreferredContentFormat() {
+        return preferredContentFormat;
+    }
+
     @Override
     public void accept(UplinkRequestVisitor visitor) {
         visitor.visit(this);
@@ -71,7 +103,9 @@ public class BootstrapRequest extends AbstractLwM2mRequest<BootstrapResponse>
     public int hashCode() {
         final int prime = 31;
         int result = 1;
+        result = prime * result + ((additionalAttributes == null) ? 0 : additionalAttributes.hashCode());
         result = prime * result + ((endpointName == null) ? 0 : endpointName.hashCode());
+        result = prime * result + ((preferredContentFormat == null) ? 0 : preferredContentFormat.hashCode());
         return result;
     }
 
@@ -84,17 +118,21 @@ public class BootstrapRequest extends AbstractLwM2mRequest<BootstrapResponse>
         if (getClass() != obj.getClass())
             return false;
         BootstrapRequest other = (BootstrapRequest) obj;
+        if (additionalAttributes == null) {
+            if (other.additionalAttributes != null)
+                return false;
+        } else if (!additionalAttributes.equals(other.additionalAttributes))
+            return false;
         if (endpointName == null) {
             if (other.endpointName != null)
                 return false;
         } else if (!endpointName.equals(other.endpointName))
             return false;
+        if (preferredContentFormat == null) {
+            if (other.preferredContentFormat != null)
+                return false;
+        } else if (!preferredContentFormat.equals(other.preferredContentFormat))
+            return false;
         return true;
     }
-
-    @Override
-    public String toString() {
-        return String.format("BootstrapRequest [endpointName=%s, additionalAttributes=%s]", endpointName,
-                additionalAttributes);
-    }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ContentFormat.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ContentFormat.java
index 87adeb4f..805aac19 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ContentFormat.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ContentFormat.java
@@ -89,6 +89,17 @@ public class ContentFormat {
         return null;
     }
 
+    /**
+     * Finds the {@link ContentFormat} for a given media type code.
+     *
+     * @return the media type or <i>unknown/unknown</i> if the given code is unknown
+     * @exception NumberFormatException if code can not be parsed as an integer.
+     */
+    public static ContentFormat fromCode(String code) throws NumberFormatException {
+        int iCode = Integer.parseInt(code);
+        return fromCode(iCode);
+    }
+
     /**
      * Finds the {@link ContentFormat} for a given media type code.
      *
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/BootstrapResource.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/BootstrapResource.java
index 963d0d2f..54fc38d4 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/BootstrapResource.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/BootstrapResource.java
@@ -26,6 +26,7 @@ import org.eclipse.californium.core.coap.Request;
 import org.eclipse.californium.core.server.resources.CoapExchange;
 import org.eclipse.leshan.core.californium.LwM2mCoapResource;
 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.BootstrapResponse;
 import org.eclipse.leshan.core.response.SendableResponse;
@@ -40,6 +41,7 @@ public class BootstrapResource extends LwM2mCoapResource {
 
     private static final Logger LOG = LoggerFactory.getLogger(BootstrapResource.class);
     private static final String QUERY_PARAM_ENDPOINT = "ep=";
+    private static final String QUERY_PARAM_PREFERRED_CONTENT_FORMAT = "pct=";
 
     private final BootstrapHandler bootstrapHandler;
 
@@ -60,12 +62,22 @@ public class BootstrapResource extends LwM2mCoapResource {
             return;
         }
 
-        // which endpoint?
+        // Get parameters
         String endpoint = null;
+        ContentFormat preferredContentFomart = null;
         Map<String, String> additionalParams = new HashMap<>();
         for (String param : request.getOptions().getUriQuery()) {
             if (param.startsWith(QUERY_PARAM_ENDPOINT)) {
                 endpoint = param.substring(QUERY_PARAM_ENDPOINT.length());
+            } else if (param.startsWith(QUERY_PARAM_PREFERRED_CONTENT_FORMAT)) {
+                try {
+                    preferredContentFomart = ContentFormat
+                            .fromCode(param.substring(QUERY_PARAM_PREFERRED_CONTENT_FORMAT.length()));
+                } catch (NumberFormatException e) {
+                    handleInvalidRequest(exchange.advanced(),
+                            "Invalid preferre content format (pct) query param : must be a number", e);
+                    return;
+                }
             } else {
                 String[] tokens = param.split("\\=");
                 if (tokens != null && tokens.length == 2) {
@@ -80,7 +92,7 @@ public class BootstrapResource extends LwM2mCoapResource {
         // handle bootstrap request
         Request coapRequest = exchange.advanced().getRequest();
         SendableResponse<BootstrapResponse> sendableResponse = bootstrapHandler.bootstrap(clientIdentity,
-                new BootstrapRequest(endpoint, additionalParams, coapRequest));
+                new BootstrapRequest(endpoint, preferredContentFomart, additionalParams, coapRequest));
         BootstrapResponse response = sendableResponse.getResponse();
         if (response.isSuccess()) {
             exchange.respond(toCoapResponseCode(response.getCode()));
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapSession.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapSession.java
index 3df6a711..5e4da0a2 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapSession.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapSession.java
@@ -37,8 +37,9 @@ public class DefaultBootstrapSession implements BootstrapSession {
     private volatile boolean cancelled = false;
 
     /**
-     * Create a {@link DefaultBootstrapSession} using default {@link ContentFormat#TLV} content format and using
-     * <code>System.currentTimeMillis()</code> to set the creation time.
+     * Create a {@link DefaultBootstrapSession} using default client preferred content format (see
+     * {@link BootstrapRequest#getPreferredContentFormat()} or {@link ContentFormat#TLV} content format if there is no
+     * preferences and using <code>System.currentTimeMillis()</code> to set the creation time.
      * 
      * @param request The bootstrap request which initiate the session.
      * @param identity The transport layer identity of the device.
@@ -47,7 +48,7 @@ public class DefaultBootstrapSession implements BootstrapSession {
      * @since 1.1
      */
     public DefaultBootstrapSession(BootstrapRequest request, Identity identity, boolean authorized) {
-        this(request, identity, authorized, ContentFormat.TLV);
+        this(request, identity, authorized, null);
     }
 
     /**
@@ -84,7 +85,16 @@ public class DefaultBootstrapSession implements BootstrapSession {
         this.endpoint = request.getEndpointName();
         this.identity = identity;
         this.authorized = authorized;
-        this.contentFormat = contentFormat;
+        if (contentFormat == null) {
+            if (request.getPreferredContentFormat() != null) {
+                System.out.println(request.getPreferredContentFormat());
+                this.contentFormat = request.getPreferredContentFormat();
+            } else {
+                this.contentFormat = ContentFormat.TLV;
+            }
+        } else {
+            this.contentFormat = contentFormat;
+        }
         this.creationTime = creationTime;
     }
 


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 a11c6897..98d21ce6 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,6 +34,7 @@ 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.ContentFormat;
 import org.eclipse.leshan.core.request.ReadRequest;
 import org.eclipse.leshan.core.response.ReadResponse;
 import org.eclipse.leshan.integration.tests.util.BootstrapIntegrationTestHelper;
@@ -83,6 +84,54 @@ public class BootstrapTest {
         helper.assertClientRegisterered();
     }
 
+    @Test
+    public void bootstrap_tlv_only() {
+        // Create DM Server without security & start it
+        helper.createServer();
+        helper.server.start();
+
+        // Create and start bootstrap server
+        helper.createBootstrapServer(null);
+        helper.bootstrapServer.start();
+
+        // Create Client and check it is not already registered
+        ContentFormat noPreferredFormat = null; // if no preferred content format server should use TLV
+        ContentFormat supportedFormat = ContentFormat.TLV;
+        helper.createClient(noPreferredFormat, supportedFormat);
+        helper.assertClientNotRegisterered();
+
+        // Start it and wait for registration
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+
+        // check the client is registered
+        helper.assertClientRegisterered();
+    }
+
+    @Test
+    public void bootstrap_senmlcbor_only() {
+        // Create DM Server without security & start it
+        helper.createServer();
+        helper.server.start();
+
+        // Create and start bootstrap server
+        helper.createBootstrapServer(null);
+        helper.bootstrapServer.start();
+
+        // Create Client and check it is not already registered
+        ContentFormat preferredFormat = ContentFormat.SENML_CBOR;
+        ContentFormat supportedFormat = ContentFormat.SENML_CBOR;
+        helper.createClient(preferredFormat, supportedFormat);
+        helper.assertClientNotRegisterered();
+
+        // Start it and wait for registration
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+
+        // check the client is registered
+        helper.assertClientRegisterered();
+    }
+
     @Test
     public void bootstrapWithAdditionalAttributes() {
         // Create DM Server without security & start it
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 af42ea38..ed1c6974 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
@@ -38,18 +38,23 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 
 import org.eclipse.californium.core.coap.Request;
 import org.eclipse.leshan.client.californium.LeshanClientBuilder;
+import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory;
 import org.eclipse.leshan.client.object.Device;
 import org.eclipse.leshan.client.object.Security;
 import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
 import org.eclipse.leshan.client.resource.ObjectsInitializer;
 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.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.util.Hex;
@@ -208,6 +213,10 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
         createClient(withoutSecurity(), null);
     }
 
+    public void createClient(ContentFormat preferredContentFormat, final ContentFormat... supportedContentFormat) {
+        createClient(withoutSecurity(), null, null, preferredContentFormat, supportedContentFormat);
+    }
+
     public void createPSKClient(String pskIdentity, byte[] pskKey) {
         // Create Security Object (with bootstrap server only)
         String bsUrl = "coaps://" + bootstrapServer.getSecuredAddress().getHostString() + ":"
@@ -229,16 +238,17 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
     }
 
     public void createClient(Security security, ObjectsInitializer initializer) {
-        createClient(security, initializer, null);
+        createClient(security, initializer, null, null);
     }
 
     @Override
     public void createClient(Map<String, String> additionalAttributes) {
-        createClient(withoutSecurity(), null, additionalAttributes);
+        createClient(withoutSecurity(), null, additionalAttributes, null);
     }
 
     public void createClient(Security security, ObjectsInitializer initializer,
-            Map<String, String> additionalAttributes) {
+            Map<String, String> additionalAttributes, ContentFormat preferredContentFormat,
+            final ContentFormat... supportedContentFormat) {
         if (initializer == null) {
             initializer = new TestObjectsInitializer();
         }
@@ -248,14 +258,34 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
         initializer.setInstancesForObject(LwM2mId.DEVICE,
                 new Device("Eclipse Leshan", IntegrationTestHelper.MODEL_NUMBER, "12345"));
         initializer.setClassForObject(LwM2mId.SERVER, DummyInstanceEnabler.class);
-        createClient(initializer, additionalAttributes);
+        createClient(initializer, additionalAttributes, preferredContentFormat, supportedContentFormat);
     }
 
-    public void createClient(ObjectsInitializer initializer, Map<String, String> additionalAttributes) {
+    public void createClient(ObjectsInitializer initializer, Map<String, String> additionalAttributes,
+            ContentFormat preferredContentFormat, final ContentFormat... supportedContentFormat) {
         // Create Leshan Client
         LeshanClientBuilder builder = new LeshanClientBuilder(getCurrentEndpoint());
         builder.setObjects(initializer.createAll());
         builder.setBootstrapAdditionalAttributes(additionalAttributes);
+        builder.setRegistrationEngineFactory(
+                new DefaultRegistrationEngineFactory().setPreferredContentFormat(preferredContentFormat));
+
+        // custom encoder/decoder with limited supported content format.
+        if (supportedContentFormat != null && supportedContentFormat.length > 0) {
+            final List<ContentFormat> supportedFormat = Arrays.asList(supportedContentFormat);
+            builder.setDecoder(new DefaultLwM2mNodeDecoder() {
+                @Override
+                public boolean isSupported(ContentFormat format) {
+                    return supportedFormat.contains(format);
+                }
+            });
+            builder.setEncoder(new DefaultLwM2mNodeEncoder() {
+                @Override
+                public boolean isSupported(ContentFormat format) {
+                    return supportedFormat.contains(format);
+                }
+            });
+        }
         client = builder.build();
         setupClientMonitoring();
     }
@@ -346,7 +376,7 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
                 dmConfig.shortId = 2222;
                 bsConfig.servers.put(0, dmConfig);
 
-                return new BootstrapConfiguration(BootstrapUtil.toRequests(bsConfig));
+                return new BootstrapConfiguration(BootstrapUtil.toRequests(bsConfig, session.getContentFormat()));
             }
         };
     }
@@ -368,7 +398,7 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
 
                 BootstrapConfig bsConfig = new BootstrapConfig();
                 bsConfig.toDelete = Arrays.asList(pathToDelete);
-                return new BootstrapConfiguration(BootstrapUtil.toRequests(bsConfig));
+                return new BootstrapConfiguration(BootstrapUtil.toRequests(bsConfig, session.getContentFormat()));
             }
         };
     }
@@ -456,7 +486,7 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
                 dmConfig.shortId = 2222;
                 bsConfig.servers.put(0, dmConfig);
 
-                return new BootstrapConfiguration(BootstrapUtil.toRequests(bsConfig));
+                return new BootstrapConfiguration(BootstrapUtil.toRequests(bsConfig, session.getContentFormat()));
             }
         };
     }
@@ -494,7 +524,7 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
                 dmConfig.shortId = 2222;
                 bsConfig.servers.put(0, dmConfig);
 
-                return new BootstrapConfiguration(BootstrapUtil.toRequests(bsConfig));
+                return new BootstrapConfiguration(BootstrapUtil.toRequests(bsConfig, session.getContentFormat()));
             }
         };
     }