Repository: eclipse/leshan
Pull request #657 (2019-02-25 17:19:11)

Add object versioning support at server side
It aims to implement  #535.


Patch:
diff --git a/leshan-bsserver-demo/src/main/java/org/eclipse/leshan/server/bootstrap/demo/LeshanBootstrapServerDemo.java b/leshan-bsserver-demo/src/main/java/org/eclipse/leshan/server/bootstrap/demo/LeshanBootstrapServerDemo.java
index ceb1aa34..ea20acd9 100755
--- a/leshan-bsserver-demo/src/main/java/org/eclipse/leshan/server/bootstrap/demo/LeshanBootstrapServerDemo.java
+++ b/leshan-bsserver-demo/src/main/java/org/eclipse/leshan/server/bootstrap/demo/LeshanBootstrapServerDemo.java
@@ -41,9 +41,9 @@ import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.servlet.ServletHolder;
 import org.eclipse.jetty.webapp.WebAppContext;
 import org.eclipse.leshan.LwM2m;
-import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectLoader;
 import org.eclipse.leshan.core.model.ObjectModel;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.server.bootstrap.demo.servlet.BootstrapServlet;
 import org.eclipse.leshan.server.bootstrap.demo.servlet.ServerServlet;
 import org.eclipse.leshan.server.californium.LeshanBootstrapServerBuilder;
@@ -174,7 +174,7 @@ public class LeshanBootstrapServerDemo {
         builder.setSecurityStore(new BootstrapConfigSecurityStore(bsStore));
         builder.setLocalAddress(localAddress, localPort);
         builder.setLocalSecureAddress(secureLocalAddress, secureLocalPort);
-        builder.setModel(new LwM2mModel(models));
+        builder.setModel(new StaticModel(models));
 
         // Create X509 credentials;
         X509Certificate serverCertificate = null;
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/impl/ObjectResource.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/impl/ObjectResource.java
index 26350d63..22d3d07c 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/impl/ObjectResource.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/impl/ObjectResource.java
@@ -34,6 +34,7 @@ import org.eclipse.leshan.client.resource.NotifySender;
 import org.eclipse.leshan.client.servers.BootstrapHandler;
 import org.eclipse.leshan.core.attributes.AttributeSet;
 import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
@@ -116,7 +117,7 @@ public class ObjectResource extends LwM2mClientCoapResource implements NotifySen
                 if (response.getCode() == org.eclipse.leshan.ResponseCode.CONTENT) {
                     LwM2mPath path = new LwM2mPath(URI);
                     LwM2mNode content = response.getContent();
-                    LwM2mModel model = new LwM2mModel(nodeEnabler.getObjectModel());
+                    LwM2mModel model = new StaticModel(nodeEnabler.getObjectModel());
                     ContentFormat format = getContentFormat(observeRequest, requestedContentFormat);
                     exchange.respond(ResponseCode.CONTENT, encoder.encode(content, format, path, model),
                             format.getCode());
@@ -133,7 +134,7 @@ public class ObjectResource extends LwM2mClientCoapResource implements NotifySen
                 if (response.getCode() == org.eclipse.leshan.ResponseCode.CONTENT) {
                     LwM2mPath path = new LwM2mPath(URI);
                     LwM2mNode content = response.getContent();
-                    LwM2mModel model = new LwM2mModel(nodeEnabler.getObjectModel());
+                    LwM2mModel model = new StaticModel(nodeEnabler.getObjectModel());
                     ContentFormat format = getContentFormat(readRequest, requestedContentFormat);
                     exchange.respond(ResponseCode.CONTENT, encoder.encode(content, format, path, model),
                             format.getCode());
@@ -195,7 +196,7 @@ public class ObjectResource extends LwM2mClientCoapResource implements NotifySen
             }
             LwM2mNode lwM2mNode;
             try {
-                LwM2mModel model = new LwM2mModel(nodeEnabler.getObjectModel());
+                LwM2mModel model = new StaticModel(nodeEnabler.getObjectModel());
                 lwM2mNode = decoder.decode(coapExchange.getRequestPayload(), contentFormat, path, model);
                 if (identity.isLwm2mBootstrapServer()) {
                     BootstrapWriteResponse response = nodeEnabler.write(identity,
@@ -255,7 +256,7 @@ public class ObjectResource extends LwM2mClientCoapResource implements NotifySen
             exchange.respond(ResponseCode.UNSUPPORTED_CONTENT_FORMAT);
             return;
         }
-        LwM2mModel model = new LwM2mModel(nodeEnabler.getObjectModel());
+        LwM2mModel model = new StaticModel(nodeEnabler.getObjectModel());
 
         // Manage Update Instance
         if (path.isObjectInstance()) {
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectsInitializer.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectsInitializer.java
index 7b934d7f..ccca6479 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectsInitializer.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectsInitializer.java
@@ -25,6 +25,7 @@ import java.util.Set;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectLoader;
 import org.eclipse.leshan.core.model.ObjectModel;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.util.Validate;
 
@@ -41,7 +42,7 @@ public class ObjectsInitializer {
 
     public ObjectsInitializer(LwM2mModel model) {
         if (model == null) {
-            this.model = new LwM2mModel(ObjectLoader.loadDefault());
+            this.model = new StaticModel(ObjectLoader.loadDefault());
         } else {
             this.model = model;
         }
diff --git a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java
index 7668361c..6cd81e35 100644
--- a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java
+++ b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java
@@ -45,9 +45,9 @@ import org.eclipse.leshan.client.californium.LeshanClientBuilder;
 import org.eclipse.leshan.client.object.Server;
 import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
 import org.eclipse.leshan.client.resource.ObjectsInitializer;
-import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectLoader;
 import org.eclipse.leshan.core.model.ObjectModel;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.core.request.BindingMode;
 import org.eclipse.leshan.util.Hex;
 import org.eclipse.leshan.util.SecurityUtil;
@@ -324,7 +324,7 @@ public class LeshanClientDemo {
         models.addAll(ObjectLoader.loadDdfResources("/models", modelPaths));
 
         // Initialize object list
-        ObjectsInitializer initializer = new ObjectsInitializer(new LwM2mModel(models));
+        ObjectsInitializer initializer = new ObjectsInitializer(new StaticModel(models));
         if (needBootstrap) {
             if (pskIdentity != null) {
                 initializer.setInstancesForObject(SECURITY, pskBootstrap(serverURI, pskIdentity, pskKey));
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/LwM2mModel.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/LwM2mModel.java
index 694124b3..62c39360 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/model/LwM2mModel.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/LwM2mModel.java
@@ -15,42 +15,12 @@
  *******************************************************************************/
 package org.eclipse.leshan.core.model;
 
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
- * A collection of LWM2M object definitions.
+ * A collection of LWM2M object definitions. This collection contains only 1 version of a specific object definition.
  */
-public class LwM2mModel {
-
-    private static final Logger LOG = LoggerFactory.getLogger(LwM2mModel.class);
-
-    private final Map<Integer, ObjectModel> objects; // objects by ID
-
-    public LwM2mModel(ObjectModel... objectModels) {
-        this(Arrays.asList(objectModels));
-    }
-
-    public LwM2mModel(Collection<ObjectModel> objectModels) {
-        if (objectModels == null) {
-            objects = new HashMap<>();
-        } else {
-            Map<Integer, ObjectModel> map = new HashMap<>();
-            for (ObjectModel model : objectModels) {
-                ObjectModel old = map.put(model.id, model);
-                if (old != null) {
-                    LOG.debug("Model already exists for object {}. Overriding it.", model.id);
-                }
-            }
-            objects = Collections.unmodifiableMap(map);
-        }
-    }
+public interface LwM2mModel {
 
     /**
      * Returns the description of a given resource.
@@ -59,13 +29,7 @@ public class LwM2mModel {
      * @param resourceId the resource identifier
      * @return the resource specification or <code>null</code> if not found
      */
-    public ResourceModel getResourceModel(int objectId, int resourceId) {
-        ObjectModel object = objects.get(objectId);
-        if (object != null) {
-            return object.resources.get(resourceId);
-        }
-        return null;
-    }
+    ResourceModel getResourceModel(int objectId, int resourceId);
 
     /**
      * Returns the description of a given object.
@@ -73,15 +37,11 @@ public class LwM2mModel {
      * @param objectId the object identifier
      * @return the object definition or <code>null</code> if not found
      */
-    public ObjectModel getObjectModel(int objectId) {
-        return objects.get(objectId);
-    }
+    ObjectModel getObjectModel(int objectId);
 
     /**
      * @return all the objects descriptions known.
      */
-    public Collection<ObjectModel> getObjectModels() {
-        return objects.values();
-    }
+    Collection<ObjectModel> getObjectModels();
 
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/LwM2mModelRepository.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/LwM2mModelRepository.java
new file mode 100644
index 00000000..e9b2142b
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/LwM2mModelRepository.java
@@ -0,0 +1,81 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.model;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.eclipse.leshan.util.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A collection of LWM2M object definitions which could contained several definitions of the same object in different
+ * version.
+ */
+public class LwM2mModelRepository {
+    private static final Logger LOG = LoggerFactory.getLogger(LwM2mModelRepository.class);
+
+    // This map contains all the object models available. Different version could be used.
+    // This map is indexed by a string composed of objectid and version (objectid##version)
+    private final Map<String, ObjectModel> objects;
+
+    public LwM2mModelRepository(ObjectModel... objectModels) {
+        this(Arrays.asList(objectModels));
+    }
+
+    public LwM2mModelRepository(Collection<ObjectModel> objectModels) {
+        if (objectModels == null) {
+            objects = new TreeMap<>();
+        } else {
+            Map<String, ObjectModel> map = new HashMap<>();
+            for (ObjectModel model : objectModels) {
+                String key = getKey(model);
+                if (key == null) {
+                    new IllegalArgumentException(String.format("Model %s is invalid : object id is missing.", model));
+                }
+                ObjectModel old = map.put(key, model);
+                if (old != null) {
+                    LOG.debug("Model already exists for object {} in version {}. Overriding it.", model.id,
+                            model.getVersion());
+                }
+            }
+            objects = Collections.unmodifiableMap(map);
+        }
+    }
+
+    public ObjectModel getObjectModel(Integer objectId, String version) {
+        Validate.notNull(objectId, "objectid must not be null");
+        Validate.notNull(version, "version must not be null");
+
+        return objects.get(getKey(objectId, version));
+    }
+
+    private String getKey(ObjectModel objectModel) {
+        return getKey(objectModel.id, objectModel.getVersion());
+    }
+
+    private String getKey(Integer objectId, String version) {
+        if (objectId == null) {
+            return null;
+        }
+        return objectId + "##" + version;
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectModel.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectModel.java
index 73a27c79..c052143f 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectModel.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectModel.java
@@ -77,6 +77,17 @@ public class ObjectModel {
         return id >= OMA_OBJECT_MIN_ID && id <= OMA_OBJECT_MAX_ID;
     }
 
+    /**
+     * @return the version and if the version is null or empty return the default value 1.0
+     * @see ObjectModel#DEFAULT_VERSION
+     */
+    public String getVersion() {
+        if (version == null || version.isEmpty()) {
+            return ObjectModel.DEFAULT_VERSION;
+        }
+        return version;
+    }
+
     @Override
     public String toString() {
         StringBuilder builder = new StringBuilder();
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/StaticModel.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/StaticModel.java
new file mode 100644
index 00000000..5d092b3a
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/StaticModel.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.model;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A static implementation of {@link LwM2mModel}.
+ */
+public class StaticModel implements LwM2mModel {
+
+    private static final Logger LOG = LoggerFactory.getLogger(StaticModel.class);
+
+    private final Map<Integer, ObjectModel> objects; // objects by ID
+
+    public StaticModel(ObjectModel... objectModels) {
+        this(Arrays.asList(objectModels));
+    }
+
+    public StaticModel(Collection<ObjectModel> objectModels) {
+        if (objectModels == null) {
+            objects = new HashMap<>();
+        } else {
+            Map<Integer, ObjectModel> map = new HashMap<>();
+            for (ObjectModel model : objectModels) {
+                ObjectModel old = map.put(model.id, model);
+                if (old != null) {
+                    LOG.debug("Model already exists for object {}. Overriding it.", model.id);
+                }
+            }
+            objects = Collections.unmodifiableMap(map);
+        }
+    }
+
+    @Override
+    public ResourceModel getResourceModel(int objectId, int resourceId) {
+        ObjectModel object = objects.get(objectId);
+        if (object != null) {
+            return object.resources.get(resourceId);
+        }
+        return null;
+    }
+
+    @Override
+    public ObjectModel getObjectModel(int objectId) {
+        return objects.get(objectId);
+    }
+
+    @Override
+    public Collection<ObjectModel> getObjectModels() {
+        return objects.values();
+    }
+}
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanBootstrapServerBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanBootstrapServerBuilder.java
index 410996a1..d68eceed 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanBootstrapServerBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanBootstrapServerBuilder.java
@@ -36,6 +36,7 @@ import org.eclipse.leshan.core.californium.DefaultEndpointFactory;
 import org.eclipse.leshan.core.californium.EndpointFactory;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectLoader;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
 import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
@@ -421,7 +422,7 @@ public class LeshanBootstrapServerBuilder {
                 }
             };
         if (model == null)
-            model = new LwM2mModel(ObjectLoader.loadDefault());
+            model = new StaticModel(ObjectLoader.loadDefault());
         if (coapConfig == null) {
             coapConfig = createDefaultNetworkConfig();
         }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/model/StandardModelProvider.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/model/StandardModelProvider.java
index 1414dee5..1be61c98 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/model/StandardModelProvider.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/model/StandardModelProvider.java
@@ -18,9 +18,11 @@ package org.eclipse.leshan.server.model;
 import org.eclipse.leshan.core.model.ObjectLoader;
 
 /**
- * A static model provider which uses the default model embedded in Leshan.
+ * A versioned model provider which uses the default model embedded in Leshan.
+ * 
+ * @see VersionedModelProvider
  */
-public class StandardModelProvider extends StaticModelProvider {
+public class StandardModelProvider extends VersionedModelProvider {
 
     public StandardModelProvider() {
         super(ObjectLoader.loadDefault());
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/model/StaticModelProvider.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/model/StaticModelProvider.java
index 5c99efcc..d70730ca 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/model/StaticModelProvider.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/model/StaticModelProvider.java
@@ -19,16 +19,18 @@ import java.util.Collection;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectModel;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.server.registration.Registration;
 
 /**
- * A LwM2mModelProvider which uses only one model for all registered clients.
+ * A LwM2mModelProvider which uses only one model for all registered clients without taking into account object version
+ * provided at registration.
  */
 public class StaticModelProvider implements LwM2mModelProvider {
     private final LwM2mModel model;
 
     public StaticModelProvider(Collection<ObjectModel> objects) {
-        this(new LwM2mModel(objects));
+        this(new StaticModel(objects));
     }
 
     public StaticModelProvider(LwM2mModel model) {
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/model/VersionedModelProvider.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/model/VersionedModelProvider.java
new file mode 100644
index 00000000..eb48103f
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/model/VersionedModelProvider.java
@@ -0,0 +1,89 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.model;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.model.LwM2mModelRepository;
+import org.eclipse.leshan.core.model.ObjectModel;
+import org.eclipse.leshan.core.model.ResourceModel;
+import org.eclipse.leshan.server.registration.Registration;
+
+/**
+ * A LwM2mModelProvider which supports object versioning. It returns a LwM2mModel taking into account object version
+ * given in registration.
+ */
+public class VersionedModelProvider implements LwM2mModelProvider {
+
+    private LwM2mModelRepository repository;
+
+    public VersionedModelProvider(Collection<ObjectModel> objectModels) {
+        this.repository = new LwM2mModelRepository(objectModels);
+    }
+
+    public VersionedModelProvider(LwM2mModelRepository repository) {
+        this.repository = repository;
+    }
+
+    @Override
+    public LwM2mModel getObjectModel(Registration registration) {
+        return new DynamicModel(registration);
+    }
+
+    private class DynamicModel implements LwM2mModel {
+
+        private final Registration registration;
+
+        public DynamicModel(Registration registration) {
+            this.registration = registration;
+        }
+
+        @Override
+        public ResourceModel getResourceModel(int objectId, int resourceId) {
+            ObjectModel objectModel = getObjectModel(objectId);
+            if (objectModel != null)
+                return objectModel.resources.get(resourceId);
+            else
+                return null;
+        }
+
+        @Override
+        public ObjectModel getObjectModel(int objectId) {
+            String version = registration.getSupportedVersion(objectId);
+            if (version != null) {
+                return repository.getObjectModel(objectId, version);
+            }
+            return null;
+        }
+
+        @Override
+        public Collection<ObjectModel> getObjectModels() {
+            Map<Integer, String> supportedObjects = registration.getSupportedObject();
+            Collection<ObjectModel> result = new ArrayList<>(supportedObjects.size());
+            for (Entry<Integer, String> supportedObject : supportedObjects.entrySet()) {
+                ObjectModel objectModel = repository.getObjectModel(supportedObject.getKey(),
+                        supportedObject.getValue());
+                if (objectModel != null)
+                    result.add(objectModel);
+            }
+            return result;
+        }
+    }
+}
\ No newline at end of file
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java
index 9571319d..afd17b79 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java
@@ -25,8 +25,13 @@ import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.eclipse.leshan.Link;
+import org.eclipse.leshan.core.attributes.Attribute;
+import org.eclipse.leshan.core.model.ObjectModel;
 import org.eclipse.leshan.core.request.BindingMode;
 import org.eclipse.leshan.core.request.Identity;
 import org.eclipse.leshan.util.StringUtils;
@@ -69,6 +74,10 @@ public class Registration implements Serializable {
 
     private final Link[] objectLinks;
 
+    // Lazy Loaded map of supported object (object id => version)
+    // built from objectLinks
+    private final AtomicReference<Map<Integer, String>> supportedObjects;
+
     private final Map<String, String> additionalRegistrationAttributes;
 
     /** The location where LWM2M objects are hosted on the device */
@@ -78,9 +87,8 @@ public class Registration implements Serializable {
 
     protected Registration(String id, String endpoint, Identity identity, String lwM2mVersion, Long lifetimeInSec,
             String smsNumber, BindingMode bindingMode, Link[] objectLinks,
-            InetSocketAddress registrationEndpointAddress,
-
-            Date registrationDate, Date lastUpdate, Map<String, String> additionalRegistrationAttributes) {
+            InetSocketAddress registrationEndpointAddress, Date registrationDate, Date lastUpdate,
+            Map<String, String> additionalRegistrationAttributes, Map<Integer, String> supportedObjects) {
 
         Validate.notNull(id);
         Validate.notEmpty(endpoint);
@@ -94,7 +102,7 @@ public class Registration implements Serializable {
         this.registrationEndpointAddress = registrationEndpointAddress;
 
         this.objectLinks = objectLinks;
-        // extract the root objects path from the object links
+        // Parse object link to extract root path.
         String rootPath = "/";
         if (objectLinks != null) {
             for (Link link : objectLinks) {
@@ -104,8 +112,10 @@ public class Registration implements Serializable {
                 }
             }
         }
+        if (!rootPath.endsWith("/"))
+            rootPath = rootPath + "/";
         this.rootPath = rootPath;
-
+        this.supportedObjects = new AtomicReference<Map<Integer, String>>(supportedObjects);
         this.lifeTimeInSec = lifetimeInSec == null ? DEFAULT_LIFETIME_IN_SEC : lifetimeInSec;
         this.lwM2mVersion = lwM2mVersion == null ? DEFAULT_LWM2M_VERSION : lwM2mVersion;
         this.bindingMode = bindingMode == null ? BindingMode.U : bindingMode;
@@ -305,6 +315,26 @@ public class Registration implements Serializable {
         return bindingMode.equals(BindingMode.UQ) || bindingMode.equals(BindingMode.UQS);
     }
 
+    /**
+     * @return the supported version of the object with the id {@code objectid}. If the object is not supported return
+     *         {@code null}
+     */
+    public String getSupportedVersion(Integer objectid) {
+        return getSupportedObject().get(objectid);
+    }
+
+    /**
+     * @return a map from {@code objectId} => {@code supportedVersion} for each supported objects. supported.
+     */
+    public Map<Integer, String> getSupportedObject() {
+        Map<Integer, String> objects = supportedObjects.get();
+        if (objects != null)
+            return objects;
+
+        supportedObjects.compareAndSet(null, Collections.unmodifiableMap(getSupportedObject(rootPath, objectLinks)));
+        return supportedObjects.get();
+    }
+
     @Override
     public String toString() {
         return String.format(
@@ -339,6 +369,46 @@ public class Registration implements Serializable {
         }
     }
 
+    /**
+     * Build a Map (object Id => object Version) from root path and registration object links.
+     */
+    public static Map<Integer, String> getSupportedObject(String rootPath, Link[] objectLinks) {
+        Map<Integer, String> objects = new HashMap<>();
+        for (Link link : objectLinks) {
+            if (link != null) {
+                Pattern p = Pattern.compile("^\\Q" + rootPath + "\\E(\\d+)(?:/\\d+)*$");
+                Matcher m = p.matcher(link.getUrl());
+                if (m.matches()) {
+                    try {
+                        // extract object id and version
+                        int objectId = Integer.parseInt(m.group(1));
+                        Object version = link.getAttributes().get(Attribute.OBJECT_VERSION);
+                        String currentVersion = objects.get(objectId);
+
+                        // store it in map
+                        if (currentVersion == null) {
+                            // we never find version for this object add it
+                            if (version instanceof String) {
+                                objects.put(objectId, (String) version);
+                            } else {
+                                objects.put(objectId, ObjectModel.DEFAULT_VERSION);
+                            }
+                        } else {
+                            // if version is already set, we override it only if new version is not DEFAULT_VERSION
+                            if (version instanceof String && !version.equals(ObjectModel.DEFAULT_VERSION)) {
+                                objects.put(objectId, (String) version);
+                            }
+                        }
+                    } catch (NumberFormatException e) {
+                        // This should not happened except maybe if the number in url is too long...
+                        // In this case we just ignore it because this is not an object id.
+                    }
+                }
+            }
+        }
+        return objects;
+    }
+
     public static class Builder {
         private final String registrationId;
         private final String endpoint;
@@ -352,6 +422,7 @@ public class Registration implements Serializable {
         private BindingMode bindingMode;
         private String lwM2mVersion;
         private Link[] objectLinks;
+        private Map<Integer, String> supportedObjects;
         private Map<String, String> additionalRegistrationAttributes;
 
         public Builder(String registrationId, String endpoint, Identity identity,
@@ -403,6 +474,11 @@ public class Registration implements Serializable {
             return this;
         }
 
+        public Builder supportedObjects(Map<Integer, String> supportedObjects) {
+            this.supportedObjects = Collections.unmodifiableMap(supportedObjects);
+            return this;
+        }
+
         public Builder additionalRegistrationAttributes(Map<String, String> additionalRegistrationAttributes) {
             this.additionalRegistrationAttributes = additionalRegistrationAttributes;
             return this;
@@ -412,9 +488,8 @@ public class Registration implements Serializable {
             return new Registration(Builder.this.registrationId, Builder.this.endpoint, Builder.this.identity,
                     Builder.this.lwM2mVersion, Builder.this.lifeTimeInSec, Builder.this.smsNumber, this.bindingMode,
                     this.objectLinks, this.registrationEndpointAddress, this.registrationDate, this.lastUpdate,
-                    this.additionalRegistrationAttributes);
+                    this.additionalRegistrationAttributes, this.supportedObjects);
         }
 
     }
-
 }
diff --git a/leshan-server-demo/logback-config.xml b/leshan-server-demo/logback-config.xml
index a1f29e6d..b082abbb 100644
--- a/leshan-server-demo/logback-config.xml
+++ b/leshan-server-demo/logback-config.xml
@@ -28,5 +28,5 @@ Contributors:
 	<logger name="org.eclipse.leshan" level="INFO"/>
 	<logger name="org.eclipse.leshan.core.californium.LwM2mCoapResource" level="DEBUG"/>
 	<logger name="org.eclipse.leshan.server.security.SecurityCheck" level="DEBUG"/>
-	<logger name="org.eclipse.leshan.core.model.LwM2mModel" level="TRACE"/>
+	<logger name="org.eclipse.leshan.core.model.LwM2mModelRepository" level="TRACE"/>
 </configuration>
\ No newline at end of file
diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java
index ebc4be56..4ebf13ac 100644
--- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java
+++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java
@@ -67,7 +67,7 @@ import org.eclipse.leshan.server.demo.servlet.SecurityServlet;
 import org.eclipse.leshan.server.demo.utils.MagicLwM2mValueConverter;
 import org.eclipse.leshan.server.impl.FileSecurityStore;
 import org.eclipse.leshan.server.model.LwM2mModelProvider;
-import org.eclipse.leshan.server.model.StaticModelProvider;
+import org.eclipse.leshan.server.model.VersionedModelProvider;
 import org.eclipse.leshan.server.redis.RedisRegistrationStore;
 import org.eclipse.leshan.server.redis.RedisSecurityStore;
 import org.eclipse.leshan.server.security.EditableSecurityStore;
@@ -361,7 +361,7 @@ public class LeshanServerDemo {
         if (modelsFolderPath != null) {
             models.addAll(ObjectLoader.loadObjectsFromDir(new File(modelsFolderPath)));
         }
-        LwM2mModelProvider modelProvider = new StaticModelProvider(models);
+        LwM2mModelProvider modelProvider = new VersionedModelProvider(models);
         builder.setObjectModelProvider(modelProvider);
 
         // Set securityStore & registrationStore
@@ -407,7 +407,8 @@ public class LeshanServerDemo {
         ServletHolder securityServletHolder = new ServletHolder(new SecurityServlet(securityStore, serverCertificate));
         root.addServlet(securityServletHolder, "/api/security/*");
 
-        ServletHolder objectSpecServletHolder = new ServletHolder(new ObjectSpecServlet(lwServer.getModelProvider()));
+        ServletHolder objectSpecServletHolder = new ServletHolder(
+                new ObjectSpecServlet(lwServer.getModelProvider(), lwServer.getRegistrationService()));
         root.addServlet(objectSpecServletHolder, "/api/objectspecs/*");
 
         // Register a service to DNS-SD
diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ObjectSpecServlet.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ObjectSpecServlet.java
index dc1f9531..0560f1ab 100644
--- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ObjectSpecServlet.java
+++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ObjectSpecServlet.java
@@ -22,9 +22,12 @@ import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.commons.lang.StringUtils;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.json.ObjectModelSerDes;
 import org.eclipse.leshan.server.model.LwM2mModelProvider;
+import org.eclipse.leshan.server.registration.Registration;
+import org.eclipse.leshan.server.registration.RegistrationService;
 
 public class ObjectSpecServlet extends HttpServlet {
 
@@ -33,21 +36,43 @@ public class ObjectSpecServlet extends HttpServlet {
     private final ObjectModelSerDes serializer;
 
     private final LwM2mModelProvider modelProvider;
+    private final RegistrationService registrationService;
 
-    public ObjectSpecServlet(LwM2mModelProvider pModelProvider) {
+    public ObjectSpecServlet(LwM2mModelProvider modelProvider, RegistrationService registrationService) {
         // use the provider from the server and return a model by client
-        modelProvider = pModelProvider;
+        this.modelProvider = modelProvider;
         serializer = new ObjectModelSerDes();
+        this.registrationService = registrationService;
     }
 
     @Override
     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+
+        // Validate path : it must be /clientEndpoint
         if (req.getPathInfo() == null) {
-            LwM2mModel model = modelProvider.getObjectModel(null);
-            resp.setContentType("application/json");
-            resp.getOutputStream().write(serializer.bSerialize(model.getObjectModels()));
-            resp.setStatus(HttpServletResponse.SC_OK);
+            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid path");
+            return;
+        }
+        String[] path = StringUtils.split(req.getPathInfo(), '/');
+        if (path.length != 1) {
+            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid path");
             return;
         }
+
+        // Get registration
+        String clientEndpoint = path[0];
+        Registration registration = registrationService.getByEndpoint(clientEndpoint);
+        if (registration == null) {
+            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            resp.getWriter().format("no registered client with id '%s'", clientEndpoint).flush();
+        }
+
+        // Get Model for this registration
+        LwM2mModel model = modelProvider.getObjectModel(registration);
+        resp.setContentType("application/json");
+        resp.getOutputStream().write(serializer.bSerialize(model.getObjectModels()));
+        resp.setStatus(HttpServletResponse.SC_OK);
+        return;
+
     }
 }
diff --git a/leshan-server-demo/src/main/resources/webapp/js/client-controllers.js b/leshan-server-demo/src/main/resources/webapp/js/client-controllers.js
index 1e7cd38c..b0791783 100644
--- a/leshan-server-demo/src/main/resources/webapp/js/client-controllers.js
+++ b/leshan-server-demo/src/main/resources/webapp/js/client-controllers.js
@@ -188,7 +188,7 @@ lwClientControllers.controller('ClientDetailCtrl', [
             $scope.client = data;
 
             // update resource tree with client details
-            lwResources.buildResourceTree($scope.client.rootPath, $scope.client.objectLinks, function (objects){
+            lwResources.buildResourceTree($scope.clientId, $scope.client.rootPath, $scope.client.objectLinks, function (objects){
                 $scope.objects = objects;
             });
 
@@ -199,7 +199,7 @@ lwClientControllers.controller('ClientDetailCtrl', [
                 $scope.$apply(function() {
                     $scope.deregistered = false;
                     $scope.client = JSON.parse(msg.data);
-                    lwResources.buildResourceTree($scope.client.rootPath, $scope.client.objectLinks, function (objects){
+                    lwResources.buildResourceTree($scope.clientId, $scope.client.rootPath, $scope.client.objectLinks, function (objects){
                         $scope.objects = objects;
                     });
                 });
diff --git a/leshan-server-demo/src/main/resources/webapp/js/lw-resources-services.js b/leshan-server-demo/src/main/resources/webapp/js/lw-resources-services.js
index b34a7e5c..1feec669 100644
--- a/leshan-server-demo/src/main/resources/webapp/js/lw-resources-services.js
+++ b/leshan-server-demo/src/main/resources/webapp/js/lw-resources-services.js
@@ -87,11 +87,11 @@ myModule.factory('lwResources',["$http", function($http) {
     /**
      * Build Resource Tree for the given rootPath and objectLinks
      */
-    var buildResourceTree = function(rootPath, objectLinks, callback) {
+    var buildResourceTree = function(endpoint, rootPath, objectLinks, callback) {
         if (objectLinks.length == 0)
             callback([]);
 
-        getObjectDefinitions(function(objectDefs){
+        getObjectDefinitions(endpoint, function(objectDefs){
             var tree = [];
 
             for (var i = 0; i < objectLinks.length; i++) {
@@ -267,31 +267,27 @@ myModule.factory('lwResources',["$http", function($http) {
     /**
      * Load all the Object Definition known by the server.
      */
-    var loadObjectDefinitions = function(callback) {
-        if (objectDefs){
-            callback(objectDefs);
-        }else{
-            $http.get("api/objectspecs")
-            .success(function(data, status, headers, config) {
-                if (data) {
-                    objectDefs = data;
-                    callback(objectDefs);
-                }else{
-                    callback([]);
-                }
-            }).error(function(data, status, headers, config) {
-                errormessage = "Unable to load object specfication : " + status +" "+ data;
-                console.error(errormessage);
+    var loadObjectDefinitions = function(endpoint, callback) {
+        $http.get("api/objectspecs/"+endpoint)
+        .success(function(data, status, headers, config) {
+            if (data) {
+                objectDefs = data;
+                callback(objectDefs);
+            }else{
                 callback([]);
-            });
-        }
+            }
+        }).error(function(data, status, headers, config) {
+            errormessage = "Unable to load object specfication : " + status +" "+ data;
+            console.error(errormessage);
+            callback([]);
+        });
     };
 
     /**
      * Return a copy of model describing the LWM2M Objects defined by OMA
      */
-    var getObjectDefinitions = function(callback) {
-        loadObjectDefinitions(function(objectDefs){
+    var getObjectDefinitions = function(endpoint,callback) {
+        loadObjectDefinitions(endpoint, function(objectDefs){
             callback($.extend(true,[],objectDefs)); // make a deep copy of the cache
         });
     };


Test patch:
diff --git a/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/BaseInstanceEnablerFactoryTest.java b/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/BaseInstanceEnablerFactoryTest.java
index 9cc53478..b733d0bd 100644
--- a/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/BaseInstanceEnablerFactoryTest.java
+++ b/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/BaseInstanceEnablerFactoryTest.java
@@ -23,15 +23,16 @@ import java.util.List;
 
 import org.eclipse.leshan.LwM2mId;
 import org.eclipse.leshan.client.resource.BaseInstanceEnablerFactory;
-import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler;
 import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
+import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectLoader;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.junit.Test;
 
 public class BaseInstanceEnablerFactoryTest {
 
-    public static final LwM2mModel model = new LwM2mModel(ObjectLoader.loadDefault());
+    public static final LwM2mModel model = new StaticModel(ObjectLoader.loadDefault());
     public static List<Integer> emptyList = Collections.emptyList();
 
     @Test(expected = IllegalStateException.class)
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java
index 47bdd548..1727f40a 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java
@@ -29,6 +29,7 @@ import org.eclipse.leshan.core.model.ObjectModel;
 import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.model.ResourceModel.Operations;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.core.node.LwM2mMultipleResource;
 import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
@@ -74,7 +75,7 @@ public class LwM2mNodeDecoderTest {
         objects.add(
                 new ObjectModel(66, "object link tests 66", "", ObjectModel.DEFAULT_VERSION, true, false, resForObj66));
 
-        model = new LwM2mModel(objects);
+        model = new StaticModel(objects);
         decoder = new DefaultLwM2mNodeDecoder();
     }
 
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoderTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoderTest.java
index 1a8d1303..a295a2dc 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoderTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoderTest.java
@@ -26,6 +26,7 @@ import java.util.Map;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectLoader;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.core.node.LwM2mMultipleResource;
 import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
@@ -49,7 +50,7 @@ public class LwM2mNodeEncoderTest {
 
     @BeforeClass
     public static void loadModel() {
-        model = new LwM2mModel(ObjectLoader.loadDefault());
+        model = new StaticModel(ObjectLoader.loadDefault());
         encoder = new DefaultLwM2mNodeEncoder();
     }
 
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/IntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/IntegrationTestHelper.java
index d4e5edb5..8b1f2efb 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/IntegrationTestHelper.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/IntegrationTestHelper.java
@@ -37,12 +37,12 @@ import org.eclipse.leshan.client.request.ServerIdentity;
 import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
 import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
 import org.eclipse.leshan.client.resource.ObjectsInitializer;
-import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectLoader;
 import org.eclipse.leshan.core.model.ObjectModel;
 import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.model.ResourceModel.Operations;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
 import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
 import org.eclipse.leshan.core.request.BindingMode;
@@ -147,7 +147,7 @@ public class IntegrationTestHelper {
 
     public void createClient(Map<String, String> additionalAttributes) {
         // Create objects Enabler
-        ObjectsInitializer initializer = new ObjectsInitializer(new LwM2mModel(createObjectModels()));
+        ObjectsInitializer initializer = new ObjectsInitializer(new StaticModel(createObjectModels()));
         initializer.setInstancesForObject(LwM2mId.SECURITY, Security.noSec(
                 "coap://" + server.getUnsecuredAddress().getHostString() + ":" + server.getUnsecuredAddress().getPort(),
                 12345));
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java
index d7318084..98e3228e 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java
@@ -41,7 +41,7 @@ import org.eclipse.californium.elements.EndpointContext;
 import org.eclipse.californium.elements.RawData;
 import org.eclipse.leshan.ResponseCode;
 import org.eclipse.leshan.client.californium.LeshanClient;
-import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
@@ -324,7 +324,7 @@ public class ObserveTest {
 
         // *** HACK send a notification with unsupported content format *** //
         byte[] payload = LwM2mNodeJsonEncoder.encode(LwM2mSingleResource.newStringResource(15, "Paris"),
-                new LwM2mPath("/3/0/15"), new LwM2mModel(helper.createObjectModels()), new LwM2mValueChecker());
+                new LwM2mPath("/3/0/15"), new StaticModel(helper.createObjectModels()), new LwM2mValueChecker());
         Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
         sendNotification(getConnector(helper.client), payload, firstCoapResponse, 666); // 666 is not a supported //
                                                                                         // contentFormat.
@@ -366,7 +366,7 @@ public class ObserveTest {
         timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp() - 2,
                 LwM2mSingleResource.newStringResource(15, "Londres")));
         byte[] payload = LwM2mNodeJsonEncoder.encodeTimestampedData(timestampedNodes, new LwM2mPath("/3/0/15"),
-                new LwM2mModel(helper.createObjectModels()), new LwM2mValueChecker());
+                new StaticModel(helper.createObjectModels()), new LwM2mValueChecker());
         Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
         sendNotification(getConnector(helper.client), payload, firstCoapResponse, ContentFormat.JSON_CODE);
         // *** Hack End *** //
@@ -409,7 +409,7 @@ public class ObserveTest {
         timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp() - 2,
                 new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Londres"))));
         byte[] payload = LwM2mNodeJsonEncoder.encodeTimestampedData(timestampedNodes, new LwM2mPath("/3/0"),
-                new LwM2mModel(helper.createObjectModels()), new LwM2mValueChecker());
+                new StaticModel(helper.createObjectModels()), new LwM2mValueChecker());
         Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
         sendNotification(getConnector(helper.client), payload, firstCoapResponse, ContentFormat.JSON_CODE);
         // *** Hack End *** //
@@ -452,7 +452,7 @@ public class ObserveTest {
         timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp() - 2,
                 new LwM2mObject(3, new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Londres")))));
         byte[] payload = LwM2mNodeJsonEncoder.encodeTimestampedData(timestampedNodes, new LwM2mPath("/3"),
-                new LwM2mModel(helper.createObjectModels()), new LwM2mValueChecker());
+                new StaticModel(helper.createObjectModels()), new LwM2mValueChecker());
 
         Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
         sendNotification(getConnector(helper.client), payload, firstCoapResponse, ContentFormat.JSON_CODE);
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeIntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeIntegrationTestHelper.java
index 1f7c301f..b9579176 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeIntegrationTestHelper.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeIntegrationTestHelper.java
@@ -29,7 +29,7 @@ import org.eclipse.leshan.client.object.Server;
 import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
 import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
 import org.eclipse.leshan.client.resource.ObjectsInitializer;
-import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.core.request.BindingMode;
 import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.server.californium.LeshanServerBuilder;
@@ -61,7 +61,7 @@ public class QueueModeIntegrationTestHelper extends IntegrationTestHelper {
     @Override
     public void createClient() {
         // Create objects Enabler
-        ObjectsInitializer initializer = new ObjectsInitializer(new LwM2mModel(createObjectModels()));
+        ObjectsInitializer initializer = new ObjectsInitializer(new StaticModel(createObjectModels()));
         initializer.setInstancesForObject(LwM2mId.SECURITY, Security.noSec(
                 "coap://" + server.getUnsecuredAddress().getHostString() + ":" + server.getUnsecuredAddress().getPort(),
                 12345));
diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilderTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilderTest.java
index adaddcca..82b5270e 100644
--- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilderTest.java
+++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilderTest.java
@@ -32,6 +32,7 @@ import org.eclipse.leshan.core.attributes.Attribute;
 import org.eclipse.leshan.core.attributes.AttributeSet;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectLoader;
+import org.eclipse.leshan.core.model.StaticModel;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
 import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
@@ -65,7 +66,7 @@ public class CoapRequestBuilderTest {
 
     @BeforeClass
     public static void loadModel() {
-        model = new LwM2mModel(ObjectLoader.loadDefault());
+        model = new StaticModel(ObjectLoader.loadDefault());
         encoder = new DefaultLwM2mNodeEncoder();
     }
 
diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/SerializationUtil.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/SerializationUtil.java
index bd96e858..23b93a83 100644
--- a/leshan-server-core/src/test/java/org/eclipse/leshan/server/SerializationUtil.java
+++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/SerializationUtil.java
@@ -20,6 +20,7 @@ import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.LinkedHashMap;
@@ -55,21 +56,40 @@ public class SerializationUtil {
             if (excludes.contains(clazz.getName())) {
                 return true;
             }
-            // we support only collection and map and we consider that checking only if parameterized type is
-            // serializable is enough.
-            if (Collection.class.isAssignableFrom(clazz) || Map.class.isAssignableFrom(clazz)) {
-                for (Type t : ptype.getActualTypeArguments()) {
-                    if (!isSerializable(t, notSerializableObject, excludes)) {
-                        notSerializableObject.put(clazz, String
-                                .format("[%s] is parameterized with not serializable type [%s].", clazz.getName(), t));
-                        return false;
+
+            if (Serializable.class.isAssignableFrom(clazz)) {
+                boolean isSerializable = true;
+                for (Field field : clazz.getDeclaredFields()) {
+                    // check if this field is Serializable
+                    if (!isSerializable(field, ptype, notSerializableObject, excludes)) {
+                        notSerializableObject.put(field, String.format("[%s %s] field from [%s] is not serializable",
+                                field.getType().getSimpleName(), field.getName(), field.getDeclaringClass().getName()));
+                        isSerializable = false;
                     }
                 }
-                return true;
+                return isSerializable;
+            }
+            // we accept Collection, Map interface because most implementation is Serializable and checking if
+            // parameterized type is serializable should enough.
+            else if (clazz.isInterface()) {
+                if (Collection.class.isAssignableFrom(clazz) || Map.class.isAssignableFrom(clazz)) {
+                    for (Type t : ptype.getActualTypeArguments()) {
+                        if (!isSerializable(t, notSerializableObject, excludes)) {
+                            notSerializableObject.put(clazz, String.format(
+                                    "[%s] is parameterized with not serializable type [%s].", clazz.getName(), t));
+                            return false;
+                        }
+                    }
+                    return true;
+                } else {
+                    notSerializableObject.put(clazz, String.format(
+                            "[%s] interface is maybe serializable but we only support Collection and Map as parameterized interface class.",
+                            clazz));
+                    return false;
+                }
             } else {
-                notSerializableObject.put(clazz, String.format(
-                        "[%s] is maybe serializable but we only support Collection and Map as parameterized class.",
-                        clazz));
+                notSerializableObject.put(clazz,
+                        String.format("[%s] is not primitive or does not implement Serializable.", clazz.getName()));
                 return false;
             }
         }
@@ -88,7 +108,7 @@ public class SerializationUtil {
             } else if (Serializable.class.isAssignableFrom(clazz)) {
                 boolean isSerializable = true;
                 for (Field field : clazz.getDeclaredFields()) {
-                    if (!isSerializable(field, notSerializableObject, excludes)) {
+                    if (!isSerializable(field, null, notSerializableObject, excludes)) {
                         notSerializableObject.put(field, String.format("[%s %s] field from [%s] is not serializable",
                                 field.getType().getSimpleName(), field.getName(), field.getDeclaringClass().getName()));
                         isSerializable = false;
@@ -100,7 +120,9 @@ public class SerializationUtil {
                         String.format("[%s] is not primitive or does not implement Serializable.", clazz.getName()));
                 return false;
             }
-        } else {
+        }
+        // others cases
+        else {
             if (excludes.contains(type.toString())) {
                 return true;
             }
@@ -109,8 +131,20 @@ public class SerializationUtil {
         }
     }
 
-    private static boolean isSerializable(Field field, Map<Object, String> notSerializableObject,
-            Collection<String> excludes) {
+    public static Type resolveType(TypeVariable<?> fieldType, ParameterizedType ptype) {
+        if (!(ptype.getRawType() instanceof Class)) {
+            return null;
+        }
+        Class<?> clazz = (Class<?>) ptype.getRawType();
+        TypeVariable<?>[] typeParameters = clazz.getTypeParameters();
+        int index = Arrays.asList(typeParameters).indexOf(fieldType);
+        if (index < 0)
+            return null;
+        return ptype.getActualTypeArguments()[index];
+    }
+
+    private static boolean isSerializable(Field field, ParameterizedType ptype,
+            Map<Object, String> notSerializableObject, Collection<String> excludes) {
 
         if (excludes.contains(field.getDeclaringClass().getName() + "." + field.getName())) {
             return true;
@@ -121,7 +155,17 @@ public class SerializationUtil {
             return true;
         }
 
-        if (isSerializable(field.getGenericType(), notSerializableObject, excludes)) {
+        Type fieldType = field.getGenericType();
+        // if this is a generic field (e.g. private V myAttribute from myClass<T>) resolve type first.
+        if (fieldType instanceof TypeVariable<?> && ptype != null) {
+            fieldType = resolveType((TypeVariable<?>) fieldType, ptype);
+            if (fieldType == null) {
+                notSerializableObject.put(field,
+                        String.format("Generic type [%s] of [%s] field from [%s] is can not be resolved",
+                                field.getGenericType(), field.getName(), field.getDeclaringClass().getName()));
+            }
+        }
+        if (isSerializable(fieldType, notSerializableObject, excludes)) {
             return true;
         }
         return false;
diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationTest.java
new file mode 100644
index 00000000..cd1a5d6f
--- /dev/null
+++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationTest.java
@@ -0,0 +1,97 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.registration;
+
+import static org.junit.Assert.*;
+
+import java.net.InetSocketAddress;
+import java.util.Map;
+
+import org.eclipse.leshan.Link;
+import org.eclipse.leshan.core.model.ObjectModel;
+import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.server.registration.Registration.Builder;
+import org.junit.Test;
+
+public class RegistrationTest {
+
+    String tto = "</>;rt=\"oma.lwm2m\";ct=100, </1/101>,</1/102>, </2/0>, </2/1> ;empty";
+
+    @Test
+    public void test_supported_object_given_an_object_link_without_version() {
+        Registration reg = given_a_registration_with_object_link_like("</1/0>,</3/0>");
+
+        // Ensure supported objects are correct
+        Map<Integer, String> supportedObject = reg.getSupportedObject();
+        assertEquals(2, supportedObject.size());
+        assertEquals(ObjectModel.DEFAULT_VERSION, supportedObject.get(1));
+        assertEquals(ObjectModel.DEFAULT_VERSION, supportedObject.get(3));
+    }
+
+    @Test
+    public void test_supported_object_given_an_object_link_with_rootpath() {
+        Registration reg = given_a_registration_with_object_link_like("</root>;rt=\"oma.lwm2m\", </root/1/0>,</3/0>");
+
+        // Ensure supported objects are correct
+        Map<Integer, String> supportedObject = reg.getSupportedObject();
+        assertEquals(1, supportedObject.size());
+        assertEquals(ObjectModel.DEFAULT_VERSION, supportedObject.get(1));
+        assertNull(supportedObject.get(3));
+    }
+
+    @Test
+    public void test_supported_object_given_an_object_link_with_regexp_rootpath() {
+        Registration reg = given_a_registration_with_object_link_like(
+                "</r(\\d+)oot>;rt=\"oma.lwm2m\", </r(\\d+)oot/1/0>,</3/0>");
+
+        // Ensure supported objects are correct
+        Map<Integer, String> supportedObject = reg.getSupportedObject();
+        assertEquals(1, supportedObject.size());
+        assertEquals(ObjectModel.DEFAULT_VERSION, supportedObject.get(1));
+        assertNull(supportedObject.get(3));
+    }
+
+    @Test
+    public void test_supported_object_given_an_object_link_with_version() {
+        Registration reg = given_a_registration_with_object_link_like("</1/0>,</3>;ver=\"1.1\",</3/0>");
+
+        // Ensure supported objects are correct
+        Map<Integer, String> supportedObject = reg.getSupportedObject();
+        assertEquals(2, supportedObject.size());
+        assertEquals(ObjectModel.DEFAULT_VERSION, supportedObject.get(1));
+        assertEquals("1.1", supportedObject.get(3));
+    }
+
+    @Test
+    public void test_supported_object_given_an_object_link_with_not_lwm2m_url() {
+        Registration reg = given_a_registration_with_object_link_like(
+                "<text>,</1/text/0/in/path>,empty,</2/O/test/in/path>,</3/0>;ver=\"1.1\",<4/0/0/>");
+
+        // Ensure supported objects are correct
+        Map<Integer, String> supportedObject = reg.getSupportedObject();
+        assertEquals(1, supportedObject.size());
+        assertEquals("1.1", supportedObject.get(3));
+    }
+
+    private Registration given_a_registration_with_object_link_like(String objectLinks) {
+        Builder builder = new Registration.Builder("id", "endpoin",
+                Identity.unsecure(InetSocketAddress.createUnresolved("localhost", 0)),
+                InetSocketAddress.createUnresolved("localhost", 0));
+
+        builder.objectLinks(Link.parse(objectLinks.getBytes()));
+        return builder.build();
+    }
+}