Repository: eclipse/leshan
Pull request #1006 (2021-05-05 17:05:25)

Add support of "ct=" attribute at client side
Add support to `ct=` attribute at **Client** side.
"ct" attribute allow to let know to the server content formats supported by the client. 

> If the LwM2M Client supports optional data formats, it MAY inform the LwM2M Server by including the content type in the root path link using the ct= link attribute. (note that the content type value 110 is the value assigned in the CoAP Content-Format Registry for the SenML JSON format used by LwM2M).
>
> </>;ct=110, </1/0>,</1/1>,</2/0>,</2/1>,</2/2>,</2/3>,</2/4>,</3/0>,</4/0>,</5>
 
 (source [core§6.2.1](http://www.openmobilealliance.org/release/LightweightM2M/V1_1_1-20190617-A/HTML-Version/OMA-TS-LightweightM2M_Core-V1_1_1-20190617-A.html#6-2-1-0-621-Register-Operation))



Since LWM2M 1.1, there is no more one mandatory content format  to rule them all (TLV) and so this kind of negotiation is more needed. 
(see https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues/514)


Patch:
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
index 505f33bc..3682a5fd 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
@@ -19,6 +19,8 @@ import java.net.InetSocketAddress;
 import java.security.cert.Certificate;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.ScheduledExecutorService;
 
 import org.eclipse.californium.core.CoapResource;
@@ -125,8 +127,10 @@ public class LeshanClient implements LwM2mClient {
         endpointsManager = createEndpointsManager(localAddress, coapConfig, dtlsConfigBuilder, trustStore,
                 endpointFactory);
         requestSender = createRequestSender(endpointsManager, sharedExecutor, encoder, objectTree.getModel());
+
         engine = engineFactory.createRegistratioEngine(endpoint, objectTree, endpointsManager, requestSender,
-                bootstrapHandler, observers, additionalAttributes, bsAdditionalAttributes, sharedExecutor);
+                bootstrapHandler, observers, additionalAttributes, bsAdditionalAttributes,
+                getSupportedContentFormat(decoder, encoder), sharedExecutor);
 
         coapServer = createCoapServer(coapConfig, sharedExecutor);
         coapServer.add(createBootstrapResource(engine, endpointsManager, bootstrapHandler));
@@ -245,6 +249,13 @@ public class LeshanClient implements LwM2mClient {
         return registrationUpdateHandler;
     }
 
+    protected Set<ContentFormat> getSupportedContentFormat(LwM2mNodeDecoder decoder, LwM2mNodeEncoder encoder) {
+        Set<ContentFormat> supportedContentFormat = new TreeSet<>();
+        supportedContentFormat.addAll(decoder.getSupportedContentFormat());
+        supportedContentFormat.addAll(encoder.getSupportedContentFormat());
+        return supportedContentFormat;
+    }
+
     @Override
     public void start() {
         LOG.info("Starting Leshan client ...");
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/RegistrationUpdateHandler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/RegistrationUpdateHandler.java
index 91e0d30b..e3125823 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/RegistrationUpdateHandler.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/RegistrationUpdateHandler.java
@@ -48,28 +48,28 @@ public class RegistrationUpdateHandler {
             public void objectInstancesRemoved(LwM2mObjectEnabler object, int... instanceIds) {
                 if (!bsHandler.isBootstrapping())
                     engine.triggerRegistrationUpdate(new RegistrationUpdate(
-                            LinkFormatHelper.getClientDescription(objecTree.getObjectEnablers().values(), null)));
+                            LinkFormatHelper.getClientDescription(objecTree.getObjectEnablers().values(), null, null)));
             }
 
             @Override
             public void objectInstancesAdded(LwM2mObjectEnabler object, int... instanceIds) {
                 if (!bsHandler.isBootstrapping())
                     engine.triggerRegistrationUpdate(new RegistrationUpdate(
-                            LinkFormatHelper.getClientDescription(objecTree.getObjectEnablers().values(), null)));
+                            LinkFormatHelper.getClientDescription(objecTree.getObjectEnablers().values(), null, null)));
             }
 
             @Override
             public void objectRemoved(LwM2mObjectEnabler object) {
                 if (!bsHandler.isBootstrapping())
                     engine.triggerRegistrationUpdate(new RegistrationUpdate(
-                            LinkFormatHelper.getClientDescription(objecTree.getObjectEnablers().values(), null)));
+                            LinkFormatHelper.getClientDescription(objecTree.getObjectEnablers().values(), null, null)));
             }
 
             @Override
             public void objectAdded(LwM2mObjectEnabler object) {
                 if (!bsHandler.isBootstrapping())
                     engine.triggerRegistrationUpdate(new RegistrationUpdate(
-                            LinkFormatHelper.getClientDescription(objecTree.getObjectEnablers().values(), null)));
+                            LinkFormatHelper.getClientDescription(objecTree.getObjectEnablers().values(), null, null)));
             }
 
             @Override
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 c5bf9700..f5e757a8 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
@@ -20,6 +20,7 @@ import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -41,6 +42,7 @@ import org.eclipse.leshan.client.servers.ServerIdentity;
 import org.eclipse.leshan.client.servers.ServerInfo;
 import org.eclipse.leshan.client.servers.ServersInfoExtractor;
 import org.eclipse.leshan.client.util.LinkFormatHelper;
+import org.eclipse.leshan.core.Link;
 import org.eclipse.leshan.core.LwM2m.Version;
 import org.eclipse.leshan.core.LwM2mId;
 import org.eclipse.leshan.core.ResponseCode;
@@ -101,6 +103,7 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
     // device state
     private final String endpoint;
     private final ContentFormat preferredContentFormat; // used for bootstrap
+    private final Set<ContentFormat> supportedContentFormats;
     private final Map<String, String> additionalAttributes;
     private final Map<String, String> bsAdditionalAttributes; // @since 1.1
     private final Map<Integer /* objectId */, LwM2mObjectEnabler> objectEnablers;
@@ -123,24 +126,13 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
     private final ScheduledExecutorService schedExecutor;
     private final boolean attachedExecutor;
 
-    public DefaultRegistrationEngine(String endpoint, LwM2mObjectTree objectTree, EndpointsManager endpointsManager,
-            LwM2mRequestSender requestSender, BootstrapHandler bootstrapState, LwM2mClientObserver observer,
-            Map<String, String> additionalAttributes, ScheduledExecutorService executor, long requestTimeoutInMs,
-            long deregistrationTimeoutInMs, int bootstrapSessionTimeoutInSec, int retryWaitingTimeInMs,
-            Integer communicationPeriodInMs, boolean reconnectOnUpdate, boolean resumeOnConnect) {
-        this(endpoint, objectTree, endpointsManager, requestSender, bootstrapState, observer, additionalAttributes,
-                null, executor, requestTimeoutInMs, deregistrationTimeoutInMs, bootstrapSessionTimeoutInSec,
-                retryWaitingTimeInMs, communicationPeriodInMs, reconnectOnUpdate, resumeOnConnect, false, null);
-    }
-
-    /** @since 1.1 */
     public DefaultRegistrationEngine(String endpoint, LwM2mObjectTree objectTree, EndpointsManager endpointsManager,
             LwM2mRequestSender requestSender, BootstrapHandler bootstrapState, LwM2mClientObserver observer,
             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,
-            ContentFormat preferredContentFormat) {
+            ContentFormat preferredContentFormat, Set<ContentFormat> supportedContentFormats) {
         this.endpoint = endpoint;
         this.objectEnablers = objectTree.getObjectEnablers();
         this.bootstrapHandler = bootstrapState;
@@ -160,6 +152,7 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
         this.resumeOnConnect = resumeOnConnect;
         this.queueMode = useQueueMode;
         this.preferredContentFormat = preferredContentFormat;
+        this.supportedContentFormats = supportedContentFormats;
 
         if (executor == null) {
             schedExecutor = createScheduledExecutor();
@@ -307,11 +300,14 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
         LOG.info("Trying to register to {} ...", server.getUri());
         RegisterRequest request = null;
         try {
+            Version lwM2mVersion = Version.lastSupported();
             EnumSet<BindingMode> supportedBindingMode = ServersInfoExtractor
                     .getDeviceSupportedBindingMode(objectEnablers.get(LwM2mId.DEVICE), 0);
-            request = new RegisterRequest(endpoint, dmInfo.lifetime, Version.lastSupported().toString(),
-                    supportedBindingMode, queueMode, null,
-                    LinkFormatHelper.getClientDescription(objectEnablers.values(), null), additionalAttributes);
+            Link[] links = LinkFormatHelper.getClientDescription(objectEnablers.values(), null,
+                    ContentFormat.getOptionalContentFormatForClient(supportedContentFormats, lwM2mVersion));
+
+            request = new RegisterRequest(endpoint, dmInfo.lifetime, lwM2mVersion.toString(), supportedBindingMode,
+                    queueMode, null, links, additionalAttributes);
             if (observer != null) {
                 observer.onRegistrationStarted(server, 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 547470d6..9fefbacb 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
@@ -16,6 +16,7 @@
 package org.eclipse.leshan.client.engine;
 
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ScheduledExecutorService;
 
 import org.eclipse.leshan.client.EndpointsManager;
@@ -49,11 +50,12 @@ public class DefaultRegistrationEngineFactory implements RegistrationEngineFacto
     public RegistrationEngine createRegistratioEngine(String endpoint, LwM2mObjectTree objectTree,
             EndpointsManager endpointsManager, LwM2mRequestSender requestSender, BootstrapHandler bootstrapState,
             LwM2mClientObserver observer, Map<String, String> additionalAttributes,
-            Map<String, String> bsAdditionalAttributes, ScheduledExecutorService sharedExecutor) {
+            Map<String, String> bsAdditionalAttributes, Set<ContentFormat> supportedContentFormat,
+            ScheduledExecutorService sharedExecutor) {
         return new DefaultRegistrationEngine(endpoint, objectTree, endpointsManager, requestSender, bootstrapState,
                 observer, additionalAttributes, bsAdditionalAttributes, sharedExecutor, requestTimeoutInMs,
                 deregistrationTimeoutInMs, bootstrapSessionTimeoutInSec, retryWaitingTimeInMs, communicationPeriodInMs,
-                reconnectOnUpdate, resumeOnConnect, queueMode, preferredContentFormat);
+                reconnectOnUpdate, resumeOnConnect, queueMode, preferredContentFormat, supportedContentFormat);
     }
 
     /**
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/RegistrationEngineFactory.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/RegistrationEngineFactory.java
index e5fcd3f7..67699e8d 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/RegistrationEngineFactory.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/RegistrationEngineFactory.java
@@ -16,6 +16,7 @@
 package org.eclipse.leshan.client.engine;
 
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ScheduledExecutorService;
 
 import org.eclipse.leshan.client.EndpointsManager;
@@ -23,6 +24,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 factory for {@link RegistrationEngine}
@@ -32,5 +34,6 @@ public interface RegistrationEngineFactory {
     RegistrationEngine createRegistratioEngine(String endpoint, LwM2mObjectTree objectTree,
             EndpointsManager endpointsManager, LwM2mRequestSender requestSender, BootstrapHandler bootstrapState,
             LwM2mClientObserver observer, Map<String, String> additionalAttributes,
-            Map<String, String> bsAdditionalAttributes, ScheduledExecutorService sharedExecutor);
+            Map<String, String> bsAdditionalAttributes, Set<ContentFormat> supportedContentFormat,
+            ScheduledExecutorService sharedExecutor);
 }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java
index 6f16d357..e250dae7 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java
@@ -21,6 +21,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
@@ -30,6 +31,7 @@ import org.eclipse.leshan.core.Link;
 import org.eclipse.leshan.core.LwM2mId;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectModel;
+import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.util.StringUtils;
 
 /**
@@ -41,7 +43,8 @@ public final class LinkFormatHelper {
     private LinkFormatHelper() {
     }
 
-    public static Link[] getClientDescription(Collection<LwM2mObjectEnabler> objectEnablers, String rootPath) {
+    public static Link[] getClientDescription(Collection<LwM2mObjectEnabler> objectEnablers, String rootPath,
+            List<ContentFormat> supportedContentFormats) {
         List<Link> links = new ArrayList<>();
 
         // clean root path
@@ -51,6 +54,18 @@ public final class LinkFormatHelper {
         String rootURL = getPath("/", root);
         Map<String, String> attributes = new HashMap<>();
         attributes.put("rt", "\"oma.lwm2m\"");
+        // serialize contentFormat;
+        if (supportedContentFormats != null && !supportedContentFormats.isEmpty()) {
+            StringBuilder b = new StringBuilder();
+            Iterator<ContentFormat> iterator = supportedContentFormats.iterator();
+            b.append(iterator.next().getCode());
+            while (iterator.hasNext()) {
+                b.append(" ");
+                b.append(iterator.next().getCode());
+            }
+            attributes.put("ct", b.toString());
+        }
+
         links.add(new Link(rootURL, attributes));
 
         // sort resources
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
index 168d68a6..a6c4be25 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
@@ -21,6 +21,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.node.LwM2mNode;
@@ -244,4 +245,9 @@ public class DefaultLwM2mNodeDecoder implements LwM2mNodeDecoder {
     public boolean isSupported(ContentFormat format) {
         return nodeDecoders.get(format) != null;
     }
+
+    @Override
+    public Set<ContentFormat> getSupportedContentFormat() {
+        return nodeDecoders.keySet();
+    };
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
index 46f6131f..b0139373 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
@@ -221,4 +221,9 @@ public class DefaultLwM2mNodeEncoder implements LwM2mNodeEncoder {
     public boolean isSupported(ContentFormat format) {
         return nodeEncoders.get(format) != null;
     }
+
+    @Override
+    public Set<ContentFormat> getSupportedContentFormat() {
+        return nodeEncoders.keySet();
+    }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoder.java
index 24c8810e..a86046b8 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoder.java
@@ -17,6 +17,7 @@ package org.eclipse.leshan.core.node.codec;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.node.LwM2mNode;
@@ -111,4 +112,9 @@ public interface LwM2mNodeDecoder {
      */
     boolean isSupported(ContentFormat format);
 
+    /**
+     * @return {@link ContentFormat} supported by this decoder
+     */
+    Set<ContentFormat> getSupportedContentFormat();
+
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoder.java
index 30ea3aa3..2ed5aa67 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoder.java
@@ -17,6 +17,7 @@ package org.eclipse.leshan.core.node.codec;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.node.LwM2mNode;
@@ -86,4 +87,9 @@ public interface LwM2mNodeEncoder {
      * return true is the given {@link ContentFormat} is supported
      */
     boolean isSupported(ContentFormat format);
+
+    /**
+     * @return {@link ContentFormat} supported by this Encoder
+     */
+    Set<ContentFormat> getSupportedContentFormat();
 }
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 805aac19..60ed54a8 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
@@ -15,10 +15,20 @@
  *******************************************************************************/
 package org.eclipse.leshan.core.request;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.leshan.core.LwM2m.Version;
+
 /**
  * Data format defined by the LWM2M specification
  */
-public class ContentFormat {
+public class ContentFormat implements Comparable<ContentFormat> {
     public static final int TLV_CODE = 11542;
     public static final int JSON_CODE = 11543;
     public static final int TEXT_CODE = 0;
@@ -33,11 +43,15 @@ public class ContentFormat {
     public static final int SENML_JSON_CODE = 110;
     public static final int SENML_CBOR_CODE = 112;
 
-    public static final ContentFormat TLV = new ContentFormat("TLV", "application/vnd.oma.lwm2m+tlv", TLV_CODE);
+    public static final ContentFormat TLV = new ContentFormat("TLV", "application/vnd.oma.lwm2m+tlv", TLV_CODE,
+            Arrays.asList(Version.V1_0));
     public static final ContentFormat JSON = new ContentFormat("JSON", "application/vnd.oma.lwm2m+json", JSON_CODE);
-    public static final ContentFormat TEXT = new ContentFormat("TEXT", "text/plain", TEXT_CODE);
-    public static final ContentFormat OPAQUE = new ContentFormat("OPAQUE", "application/octet-stream", OPAQUE_CODE);
-    public static final ContentFormat LINK = new ContentFormat("LINK", "application/link-format", LINK_CODE);
+    public static final ContentFormat TEXT = new ContentFormat("TEXT", "text/plain", TEXT_CODE,
+            Arrays.asList(Version.V1_1));
+    public static final ContentFormat OPAQUE = new ContentFormat("OPAQUE", "application/octet-stream", OPAQUE_CODE,
+            Arrays.asList(Version.V1_1));
+    public static final ContentFormat LINK = new ContentFormat("LINK", "application/link-format", LINK_CODE,
+            Arrays.asList(Version.V1_1));
     public static final ContentFormat SENML_JSON = new ContentFormat("SENML_JSON", "application/senml+json",
             SENML_JSON_CODE);
     public static final ContentFormat SENML_CBOR = new ContentFormat("SENML_CBOR", "application/senml+cbor",
@@ -46,23 +60,33 @@ public class ContentFormat {
 
     public static final ContentFormat DEFAULT = TLV;
 
-    private static final ContentFormat knownContentFormat[] = new ContentFormat[] { TLV, JSON, SENML_JSON, SENML_CBOR,
+    public static final ContentFormat knownContentFormat[] = new ContentFormat[] { TLV, JSON, SENML_JSON, SENML_CBOR,
                             TEXT, OPAQUE, CBOR, LINK };
 
     private final String name;
     private final String mediaType;
     private final int code;
+    private Set<Version> mandatoryForClient; // lwm2m version where this content format is mandatory at client side
+
+    public ContentFormat(String name, String mediaType, int code, Collection<Version> mandatory) {
+        this.name = name;
+        this.mediaType = mediaType;
+        this.code = code;
+        this.mandatoryForClient = new HashSet<>(mandatory);
+    }
 
     public ContentFormat(String name, String mediaType, int code) {
         this.name = name;
         this.mediaType = mediaType;
         this.code = code;
+        this.mandatoryForClient = Collections.emptySet();
     }
 
     public ContentFormat(int code) {
         this.name = "UNKNOWN";
         this.mediaType = "unknown/unknown";
         this.code = code;
+        this.mandatoryForClient = Collections.emptySet();
     }
 
     public String getName() {
@@ -77,6 +101,13 @@ public class ContentFormat {
         return this.code;
     }
 
+    /**
+     * @return True is this {@link ContentFormat} is mandatory at client side for the given LWM2M {@link Version}.
+     */
+    public boolean isMandatoryForClient(Version lwM2mVersion) {
+        return this.mandatoryForClient.contains(lwM2mVersion);
+    }
+
     /**
      * Find the {@link ContentFormat} for the given media type (<code>null</code> if not found)
      */
@@ -154,4 +185,28 @@ public class ContentFormat {
             return false;
         return true;
     }
+
+    @Override
+    public int compareTo(ContentFormat ct) {
+        return Integer.compare(this.code, ct.code);
+    }
+
+    /**
+     * From a list of {@link ContentFormat} of a client, return only the optional ones for a given LWM2M
+     * {@link Version}. In other words we remove all {@link ContentFormat} which is considered as Mandatory.
+     * 
+     * @param contentFormat A list of all supported {@link ContentFormat} for a given device.
+     * @param lwm2mVersion The LWM2M version targeted
+     * @return only optional {@link ContentFormat}
+     */
+    public static List<ContentFormat> getOptionalContentFormatForClient(Collection<ContentFormat> contentFormat,
+            Version lwm2mVersion) {
+        List<ContentFormat> optionalFormat = new ArrayList<>();
+        for (ContentFormat supportedFormat : contentFormat) {
+            if (!supportedFormat.isMandatoryForClient(lwm2mVersion)) {
+                optionalFormat.add(supportedFormat);
+            }
+        }
+        return optionalFormat;
+    }
 }


Test patch:
diff --git a/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java b/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java
index 2f36a430..cf0ef1b5 100644
--- a/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java
+++ b/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java
@@ -15,9 +15,11 @@
  *******************************************************************************/
 package org.eclipse.leshan.client.util;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -111,7 +113,7 @@ public class LinkFormatHelperTest {
         instancesMap.put(0, new BaseInstanceEnabler());
         objectEnablers.add(new ObjectEnabler(6, getObjectModel(6), instancesMap, null, ContentFormat.DEFAULT));
 
-        Link[] links = LinkFormatHelper.getClientDescription(objectEnablers, null);
+        Link[] links = LinkFormatHelper.getClientDescription(objectEnablers, null, null);
         String strLinks = Link.serialize(links);
 
         assertEquals("</>;rt=\"oma.lwm2m\",</6/0>", strLinks);
@@ -127,7 +129,7 @@ public class LinkFormatHelperTest {
         objectEnablers.add(
                 new ObjectEnabler(6, getVersionedObjectModel(6, "2.0"), instancesMap, null, ContentFormat.DEFAULT));
 
-        Link[] links = LinkFormatHelper.getClientDescription(objectEnablers, null);
+        Link[] links = LinkFormatHelper.getClientDescription(objectEnablers, null, null);
         String strLinks = Link.serialize(links);
 
         assertEquals("</>;rt=\"oma.lwm2m\",</6>;ver=2.0,</6/0>,</6/1>", strLinks);
@@ -141,12 +143,37 @@ public class LinkFormatHelperTest {
         objectEnablers.add(
                 new ObjectEnabler(6, getVersionedObjectModel(6, "2.0"), instancesMap, null, ContentFormat.DEFAULT));
 
-        Link[] links = LinkFormatHelper.getClientDescription(objectEnablers, null);
+        Link[] links = LinkFormatHelper.getClientDescription(objectEnablers, null, null);
         String strLinks = Link.serialize(links);
 
         assertEquals("</>;rt=\"oma.lwm2m\",</6>;ver=2.0", strLinks);
     }
 
+    @Test
+    public void encode_1_content_format() {
+        List<LwM2mObjectEnabler> objectEnablers = new ArrayList<>();
+        Map<Integer, LwM2mInstanceEnabler> instancesMap = Collections.emptyMap();
+        objectEnablers.add(
+                new ObjectEnabler(6, getVersionedObjectModel(6, "1.0"), instancesMap, null, ContentFormat.DEFAULT));
+
+        Link[] links = LinkFormatHelper.getClientDescription(objectEnablers, null, Arrays.asList(ContentFormat.TLV));
+
+        assertArrayEquals(Link.parse("</>;rt=\"oma.lwm2m\";ct=11542,</6>".getBytes()), links);
+    }
+
+    @Test
+    public void encode_several_content_format() {
+        List<LwM2mObjectEnabler> objectEnablers = new ArrayList<>();
+        Map<Integer, LwM2mInstanceEnabler> instancesMap = Collections.emptyMap();
+        objectEnablers.add(
+                new ObjectEnabler(6, getVersionedObjectModel(6, "1.0"), instancesMap, null, ContentFormat.DEFAULT));
+
+        Link[] links = LinkFormatHelper.getClientDescription(objectEnablers, null,
+                Arrays.asList(ContentFormat.TLV, ContentFormat.JSON, ContentFormat.OPAQUE));
+
+        assertArrayEquals(Link.parse("</>;rt=\"oma.lwm2m\";ct=11542 11543 42,</6>".getBytes()), links);
+    }
+
     @Test
     public void encode_bootstrap_object() {
         Map<Integer, LwM2mInstanceEnabler> instancesMap = new HashMap<>();
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/request/ContentFormatTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/request/ContentFormatTest.java
new file mode 100644
index 00000000..6f25adb5
--- /dev/null
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/request/ContentFormatTest.java
@@ -0,0 +1,47 @@
+/*******************************************************************************
+ * Copyright (c) 2021 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 static org.eclipse.leshan.core.request.ContentFormat.*;
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.leshan.core.LwM2m.Version;
+import org.eclipse.leshan.core.node.codec.CodecException;
+import org.junit.Test;
+
+public class ContentFormatTest {
+
+    @Test
+    public void get_optional_content_format_for_v1_0() throws CodecException {
+
+        List<ContentFormat> optionalContentFormat = ContentFormat
+                .getOptionalContentFormatForClient(Arrays.asList(TLV, JSON, TEXT, OPAQUE), Version.V1_0);
+
+        assertEquals(Arrays.asList(JSON, TEXT, OPAQUE), optionalContentFormat);
+    }
+
+    @Test
+    public void get_optional_content_format_for_v1_1() throws CodecException {
+        List<ContentFormat> optionalContentFormat = ContentFormat.getOptionalContentFormatForClient(
+                Arrays.asList(TLV, JSON, SENML_JSON, SENML_CBOR, TEXT, OPAQUE, CBOR, LINK), Version.V1_1);
+
+        assertEquals(Arrays.asList(TLV, JSON, SENML_JSON, SENML_CBOR, CBOR), optionalContentFormat);
+
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeTest.java
index 3f22a7e9..edf15d38 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeTest.java
@@ -60,8 +60,9 @@ public class QueueModeTest {
 
         // Check client is well registered
         queueModeHelper.assertClientRegisterered();
-        assertArrayEquals(
-                Link.parse("</>;rt=\"oma.lwm2m\",</1>;ver=1.1,</1/0>,</2>,</3>;ver=1.1,</3/0>,</2000/0>".getBytes()),
+        assertArrayEquals(Link.parse(
+                "</>;rt=\"oma.lwm2m\";ct=60 110 112 11542 11543,</1>;ver=1.1,</1/0>,</2>,</3>;ver=1.1,</3/0>,</2000/0>"
+                        .getBytes()),
                 queueModeHelper.getCurrentRegistration().getObjectLinks());
 
         // Wait for client awake time expiration (20% margin)
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RegistrationTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RegistrationTest.java
index 59f0b2e1..8e357671 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RegistrationTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RegistrationTest.java
@@ -85,7 +85,8 @@ public class RegistrationTest {
         // Check client is well registered
         helper.assertClientRegisterered();
         assertArrayEquals(Link.parse(
-                "</>;rt=\"oma.lwm2m\",</1>;ver=1.1,</1/0>,</2>,</3>;ver=1.1,</3/0>,</2000/0>,</2000/1>".getBytes()),
+                "</>;rt=\"oma.lwm2m\";ct=60 110 112 1542 1543 11542 11543,</1>;ver=1.1,</1/0>,</2>,</3>;ver=1.1,</3/0>,</2000/0>,</2000/1>"
+                        .getBytes()),
                 helper.getCurrentRegistration().getObjectLinks());
 
         // Check for update
@@ -110,7 +111,8 @@ public class RegistrationTest {
         // Check client is well registered
         helper.assertClientRegisterered();
         assertArrayEquals(Link.parse(
-                "</>;rt=\"oma.lwm2m\",</1>;ver=1.1,</1/0>,</2>,</3>;ver=1.1,</3/0>,</2000/0>,</2000/1>".getBytes()),
+                "</>;rt=\"oma.lwm2m\";ct=60 110 112 1542 1543 11542 11543,</1>;ver=1.1,</1/0>,</2>,</3>;ver=1.1,</3/0>,</2000/0>,</2000/1>"
+                        .getBytes()),
                 helper.getCurrentRegistration().getObjectLinks());
 
         // Stop client with out de-registration
@@ -260,7 +262,8 @@ public class RegistrationTest {
         assertNotNull(helper.getLastRegistration());
         assertEquals(additionalAttributes, helper.getLastRegistration().getAdditionalRegistrationAttributes());
         assertArrayEquals(Link.parse(
-                "</>;rt=\"oma.lwm2m\",</1>;ver=1.1,</1/0>,</2>,</3>;ver=1.1,</3/0>,</2000/0>,</2000/1>".getBytes()),
+                "</>;rt=\"oma.lwm2m\";ct=60 110 112 1542 1543 11542 11543,</1>;ver=1.1,</1/0>,</2>,</3>;ver=1.1,</3/0>,</2000/0>,</2000/1>"
+                        .getBytes()),
                 helper.getCurrentRegistration().getObjectLinks());
     }