Repository: eclipse/leshan
Pull request #763 (2019-10-28 17:08:01)

Valide id in LWM2MNode and LWM2MPath
Ensure ID is valid + some more validation.


Patch:
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mIncompletePath.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mIncompletePath.java
new file mode 100644
index 00000000..00389cbd
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mIncompletePath.java
@@ -0,0 +1,99 @@
+/*******************************************************************************
+ * 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.node;
+
+/**
+ * A path pointing to a LwM2M node (root, object, object instance, resource or resource instance) but with missing
+ * instance id part.
+ * <p>
+ * This is mainly used for CREATE request where object instance id could be omitted and so client choose it.
+ */
+public class LwM2mIncompletePath extends LwM2mPath {
+
+    /**
+     * Create a path to an object instance without object instance id
+     *
+     * @param objectId the object identifier
+     */
+    public LwM2mIncompletePath(int objectId) {
+        super(objectId, LwM2mObjectInstance.UNDEFINED, null, null);
+        validate();
+    }
+
+    /**
+     * Create a path to a resource of a given object instance without object instance id
+     *
+     * @param objectId the object identifier
+     * @param resourceId the resource identifier
+     */
+    public LwM2mIncompletePath(int objectId, int resourceId) {
+        super(objectId, LwM2mObjectInstance.UNDEFINED, resourceId, null);
+        validate();
+    }
+
+    /**
+     * Create a path to a resource instance of a given resource without object instance id
+     *
+     * @param objectId the object identifier
+     * @param resourceId the resource identifier
+     * @param resourceInstanceId the resource instance identifier
+     */
+    public LwM2mIncompletePath(int objectId, int resourceId, int resourceInstanceId) {
+        super(objectId, LwM2mObjectInstance.UNDEFINED, resourceId, resourceInstanceId);
+        validate();
+    }
+
+    @Override
+    public LwM2mPath append(String path) {
+        throw new UnsupportedOperationException("not implemented");
+    }
+
+    @Override
+    public LwM2mPath append(int end) {
+        if (isObjectInstance()) {
+            return new LwM2mIncompletePath(getObjectId(), end);
+        } else if (isResource()) {
+            return new LwM2mIncompletePath(getObjectId(), getResourceId(), end);
+        } else {
+            throw new IllegalArgumentException(String.format(
+                    "Unable to append Id(%d) to path %s. Resource instance level is the deeper one.", end, this));
+        }
+    }
+
+    @Override
+    protected void validate() {
+        LwM2mNodeUtil.validateIncompletePath(this);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder b = new StringBuilder();
+        b.append("/");
+        if (getObjectId() != null) {
+            b.append(getObjectId());
+            if (getObjectInstanceId() != null) {
+                b.append("/undefined");
+                if (getResourceId() != null) {
+                    b.append("/").append(getResourceId());
+                    if (getResourceInstanceId() != null) {
+                        b.append("/").append(getResourceInstanceId());
+                    }
+                }
+            }
+        }
+        return b.toString();
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mMultipleResource.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mMultipleResource.java
index f70dd5ff..754843d7 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mMultipleResource.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mMultipleResource.java
@@ -27,7 +27,6 @@ import java.util.NoSuchElementException;
 import java.util.Objects;
 
 import org.eclipse.leshan.core.model.ResourceModel.Type;
-import org.eclipse.leshan.util.Validate;
 
 /**
  * A resource which contains several resource instances.
@@ -43,72 +42,79 @@ public class LwM2mMultipleResource implements LwM2mResource {
     private final Type type;
 
     protected LwM2mMultipleResource(int id, Map<Integer, ?> values, Type type) {
+        LwM2mNodeUtil.validateNotNull(values, "values MUST NOT be null");
+        LwM2mNodeUtil.validateResourceId(id);
+
+        for (Integer instanceId : values.keySet()) {
+            LwM2mNodeUtil.validateResourceInstanceId(instanceId);
+        }
         this.id = id;
         this.values = Collections.unmodifiableMap(new HashMap<>(values));
         this.type = type;
     }
 
     public static LwM2mMultipleResource newResource(int id, Map<Integer, ?> values, Type type) {
+        LwM2mNodeUtil.validateNotNull(values, "values MUST NOT be null");
         switch (type) {
         case INTEGER:
-            Validate.allElementsOfType(values.values(), Long.class);
+            LwM2mNodeUtil.allElementsOfType(values.values(), Long.class);
             break;
         case FLOAT:
-            Validate.allElementsOfType(values.values(), Double.class);
+            LwM2mNodeUtil.allElementsOfType(values.values(), Double.class);
             break;
         case BOOLEAN:
-            Validate.allElementsOfType(values.values(), Boolean.class);
+            LwM2mNodeUtil.allElementsOfType(values.values(), Boolean.class);
             break;
         case OPAQUE:
-            Validate.allElementsOfType(values.values(), byte[].class);
+            LwM2mNodeUtil.allElementsOfType(values.values(), byte[].class);
             break;
         case STRING:
-            Validate.allElementsOfType(values.values(), String.class);
+            LwM2mNodeUtil.allElementsOfType(values.values(), String.class);
             break;
         case TIME:
-            Validate.allElementsOfType(values.values(), Date.class);
+            LwM2mNodeUtil.allElementsOfType(values.values(), Date.class);
             break;
         case OBJLNK:
-            Validate.allElementsOfType(values.values(), ObjectLink.class);
+            LwM2mNodeUtil.allElementsOfType(values.values(), ObjectLink.class);
             break;
         default:
-            throw new IllegalArgumentException(String.format("Type %s is not supported", type.name()));
+            throw new LwM2mNodeException(String.format("Type %s is not supported", type.name()));
         }
         return new LwM2mMultipleResource(id, values, type);
     }
 
     public static LwM2mMultipleResource newStringResource(int id, Map<Integer, String> values) {
-        Validate.noNullElements(values.values());
+        LwM2mNodeUtil.noNullElements(values.values());
         return new LwM2mMultipleResource(id, values, Type.STRING);
     }
 
     public static LwM2mMultipleResource newIntegerResource(int id, Map<Integer, Long> values) {
-        Validate.noNullElements(values.values());
+        LwM2mNodeUtil.noNullElements(values.values());
         return new LwM2mMultipleResource(id, values, Type.INTEGER);
     }
 
     public static LwM2mMultipleResource newBooleanResource(int id, Map<Integer, Boolean> values) {
-        Validate.noNullElements(values.values());
+        LwM2mNodeUtil.noNullElements(values.values());
         return new LwM2mMultipleResource(id, values, Type.BOOLEAN);
     }
 
     public static LwM2mMultipleResource newFloatResource(int id, Map<Integer, Double> values) {
-        Validate.noNullElements(values.values());
+        LwM2mNodeUtil.noNullElements(values.values());
         return new LwM2mMultipleResource(id, values, Type.FLOAT);
     }
 
     public static LwM2mMultipleResource newDateResource(int id, Map<Integer, Date> values) {
-        Validate.noNullElements(values.values());
+        LwM2mNodeUtil.noNullElements(values.values());
         return new LwM2mMultipleResource(id, values, Type.TIME);
     }
 
     public static LwM2mMultipleResource newObjectLinkResource(int id, Map<Integer, ObjectLink> values) {
-        Validate.noNullElements(values.values());
+        LwM2mNodeUtil.noNullElements(values.values());
         return new LwM2mMultipleResource(id, values, Type.OBJLNK);
     }
 
     public static LwM2mMultipleResource newBinaryResource(int id, Map<Integer, byte[]> values) {
-        Validate.noNullElements(values.values());
+        LwM2mNodeUtil.noNullElements(values.values());
         return new LwM2mMultipleResource(id, values, Type.OPAQUE);
     }
 
@@ -241,8 +247,8 @@ public class LwM2mMultipleResource implements LwM2mResource {
                         return false;
                 } else {
                     // Custom equals to handle byte arrays
-                    return type == Type.OPAQUE ? Arrays.equals((byte[]) value, (byte[]) m2.get(key)) : value.equals(m2
-                            .get(key));
+                    return type == Type.OPAQUE ? Arrays.equals((byte[]) value, (byte[]) m2.get(key))
+                            : value.equals(m2.get(key));
                 }
             }
         } catch (ClassCastException | NullPointerException unused) {
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeException.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeException.java
new file mode 100644
index 00000000..b4d42900
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeException.java
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * 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.node;
+
+public class LwM2mNodeException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    public LwM2mNodeException(String message) {
+        super(message);
+    }
+
+    public LwM2mNodeException(String message, Object... args) {
+        super(String.format(message, args));
+    }
+
+    public LwM2mNodeException(Exception e, String message, Object... args) {
+        super(String.format(message, args), e);
+    }
+
+    public LwM2mNodeException(String message, Exception cause) {
+        super(message, cause);
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeUtil.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeUtil.java
new file mode 100644
index 00000000..a6c5d3d0
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeUtil.java
@@ -0,0 +1,143 @@
+/*******************************************************************************
+ * 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.node;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+/**
+ * Utility class about {@link LwM2mNode} and {@link LwM2mPath}.
+ */
+public class LwM2mNodeUtil {
+
+    public static void validateNotNull(Object value, String message, Object... args) {
+        if (value == null) {
+            throw new LwM2mNodeException(message, args);
+        }
+    }
+
+    public static void allElementsOfType(Collection<?> collection, Class<?> clazz) {
+        int i = 0;
+        for (Iterator<?> it = collection.iterator(); it.hasNext(); i++) {
+            if (clazz.isInstance(it.next()) == false) {
+                throw new LwM2mNodeException("The validated collection contains an element not of type "
+                        + clazz.getName() + " at index: " + i);
+            }
+        }
+    }
+
+    public static void noNullElements(Collection<?> collection) {
+        validateNotNull(collection, "collection MUST NOT be null");
+        int i = 0;
+        for (Iterator<?> it = collection.iterator(); it.hasNext(); i++) {
+            if (it.next() == null) {
+                throw new LwM2mNodeException("The validated collection contains null element at index: " + i);
+            }
+        }
+    }
+
+    public static boolean isUnsignedInt(Integer id) {
+        return id != null && 0 <= id && id <= 65535;
+    }
+
+    public static boolean isValidObjectId(Integer id) {
+        return isUnsignedInt(id);
+    }
+
+    public static void validateObjectId(Integer id) {
+        if (!isValidObjectId(id)) {
+            throw new LwM2mNodeException("Invalid object id %d, It MUST be an unsigned int.", id);
+        }
+    }
+
+    public static boolean isValidObjectInstanceId(Integer id) {
+        // MAX_ID 65535 is a reserved value and MUST NOT be used for identifying an Object Instance.
+        return id != null && 0 <= id && id <= 65534;
+    }
+
+    public static void validateObjectInstanceId(Integer id) {
+        if (!isValidObjectInstanceId(id)) {
+            throw new LwM2mNodeException(
+                    "Invalid object instance id %d, It MUST be an unsigned int. (65535 is reserved)", id);
+        }
+    }
+
+    public static boolean isValidResourceId(Integer id) {
+        return isUnsignedInt(id);
+    }
+
+    public static void validateResourceId(Integer id) {
+        if (!isValidResourceId(id)) {
+            throw new LwM2mNodeException("Invalid resource id %d, It MUST be an unsigned int.", id);
+        }
+    }
+
+    public static boolean isValidResourceInstanceId(Integer id) {
+        return isUnsignedInt(id);
+    }
+
+    public static void validateResourceInstanceId(Integer id) {
+        if (!isValidResourceInstanceId(id)) {
+            throw new LwM2mNodeException("Invalid resource instance id %d, It MUST be an unsigned int.", id);
+        }
+    }
+
+    public static void validatePath(LwM2mPath path) {
+        if (path.isObject()) {
+            LwM2mNodeUtil.validateObjectId(path.getObjectId());
+        } else if (path.isObjectInstance()) {
+            LwM2mNodeUtil.validateObjectId(path.getObjectId());
+            LwM2mNodeUtil.validateObjectInstanceId(path.getObjectInstanceId());
+        } else if (path.isResource()) {
+            LwM2mNodeUtil.validateObjectId(path.getObjectId());
+            LwM2mNodeUtil.validateObjectInstanceId(path.getObjectInstanceId());
+            LwM2mNodeUtil.validateResourceId(path.getResourceId());
+        } else if (path.isResourceInstance()) {
+            LwM2mNodeUtil.validateObjectId(path.getObjectId());
+            LwM2mNodeUtil.validateObjectInstanceId(path.getObjectInstanceId());
+            LwM2mNodeUtil.validateResourceId(path.getResourceId());
+            LwM2mNodeUtil.validateResourceInstanceId(path.getResourceInstanceId());
+        } else if (!path.isRoot()) {
+            throw new LwM2mNodeException("Invalid LWM2M path (%d,%d,%d,%d)", path.getObjectId(),
+                    path.getObjectInstanceId(), path.getResourceId(), path.getResourceInstanceId());
+        }
+    }
+
+    public static void validateIncompletePath(LwM2mPath path) {
+        if (path.isObjectInstance()) {
+            LwM2mNodeUtil.validateObjectId(path.getObjectId());
+            LwM2mNodeUtil.validateUndefinedObjecInstanceId(path.getObjectInstanceId());
+        } else if (path.isResource()) {
+            LwM2mNodeUtil.validateObjectId(path.getObjectId());
+            LwM2mNodeUtil.validateUndefinedObjecInstanceId(path.getObjectInstanceId());
+            LwM2mNodeUtil.validateResourceId(path.getResourceId());
+        } else if (path.isResourceInstance()) {
+            LwM2mNodeUtil.validateObjectId(path.getObjectId());
+            LwM2mNodeUtil.validateUndefinedObjecInstanceId(path.getObjectInstanceId());
+            LwM2mNodeUtil.validateResourceId(path.getResourceId());
+            LwM2mNodeUtil.validateResourceInstanceId(path.getResourceInstanceId());
+        } else if (!path.isRoot()) {
+            throw new LwM2mNodeException("Invalid LWM2M path (%d,%d,%d,%d)", path.getObjectId(),
+                    path.getObjectInstanceId(), path.getResourceId(), path.getResourceInstanceId());
+        }
+    }
+
+    public static void validateUndefinedObjecInstanceId(int id) {
+        if (id != LwM2mObjectInstance.UNDEFINED) {
+            throw new LwM2mNodeException("Instance id should be undefined");
+        }
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mObject.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mObject.java
index 6e76f8b4..7e960455 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mObject.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mObject.java
@@ -21,8 +21,6 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
-import org.eclipse.leshan.util.Validate;
-
 /**
  * The top level element in the LWM2M resource tree.
  * <p>
@@ -36,12 +34,18 @@ public class LwM2mObject implements LwM2mNode {
     private final Map<Integer, LwM2mObjectInstance> instances;
 
     public LwM2mObject(int id, Collection<LwM2mObjectInstance> instances) {
-        Validate.notNull(instances);
+        LwM2mNodeUtil.validateNotNull(instances, "instances MUST NOT be null");
+        LwM2mNodeUtil.validateObjectId(id);
 
         this.id = id;
         HashMap<Integer, LwM2mObjectInstance> instancesMap = new HashMap<>(instances.size());
         for (LwM2mObjectInstance instance : instances) {
-            instancesMap.put(instance.getId(), instance);
+            LwM2mObjectInstance previous = instancesMap.put(instance.getId(), instance);
+            if (previous != null) {
+                throw new LwM2mNodeException(
+                        "Unable to create LwM2mObject : there is several instances with the same id %d",
+                        instance.getId());
+            }
         }
         this.instances = Collections.unmodifiableMap(instancesMap);
     }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mObjectInstance.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mObjectInstance.java
index a31f8969..06b5ac4a 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mObjectInstance.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mObjectInstance.java
@@ -35,8 +35,24 @@ public class LwM2mObjectInstance implements LwM2mNode {
 
     private final Map<Integer, LwM2mResource> resources;
 
+    public LwM2mObjectInstance(Collection<LwM2mResource> resources) {
+        LwM2mNodeUtil.validateNotNull(resources, "resource MUST NOT be null");
+        this.id = UNDEFINED;
+        Map<Integer, LwM2mResource> resourcesMap = new HashMap<>(resources.size());
+        for (LwM2mResource resource : resources) {
+            LwM2mResource previous = resourcesMap.put(resource.getId(), resource);
+            if (previous != null) {
+                throw new LwM2mNodeException(
+                        "Unable to create LwM2mObjectInstance : there is several resources with the same id %d",
+                        resource.getId());
+            }
+        }
+        this.resources = Collections.unmodifiableMap(resourcesMap);
+    }
+
     public LwM2mObjectInstance(int id, Collection<LwM2mResource> resources) {
         Validate.notNull(resources);
+        LwM2mNodeUtil.validateObjectInstanceId(id);
 
         this.id = id;
         Map<Integer, LwM2mResource> resourcesMap = new HashMap<>(resources.size());
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mPath.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mPath.java
index f809953e..8c0dd90d 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mPath.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mPath.java
@@ -30,10 +30,7 @@ public class LwM2mPath {
     public final static LwM2mPath ROOTPATH = new LwM2mPath();
 
     private LwM2mPath() {
-        this.objectId = null;
-        this.objectInstanceId = null;
-        this.resourceId = null;
-        this.resourceInstanceId = null;
+        this(null, null, null, null);
     }
 
     /**
@@ -42,10 +39,8 @@ public class LwM2mPath {
      * @param objectId the object identifier
      */
     public LwM2mPath(int objectId) {
-        this.objectId = objectId;
-        this.objectInstanceId = null;
-        this.resourceId = null;
-        this.resourceInstanceId = null;
+        this(objectId, null, null, null);
+        validate();
     }
 
     /**
@@ -55,10 +50,8 @@ public class LwM2mPath {
      * @param objectInstanceId the instance identifier
      */
     public LwM2mPath(int objectId, int objectInstanceId) {
-        this.objectId = objectId;
-        this.objectInstanceId = objectInstanceId;
-        this.resourceId = null;
-        this.resourceInstanceId = null;
+        this(objectId, objectInstanceId, null, null);
+        validate();
     }
 
     /**
@@ -69,10 +62,8 @@ public class LwM2mPath {
      * @param resourceId the resource identifier
      */
     public LwM2mPath(int objectId, int objectInstanceId, int resourceId) {
-        this.objectId = objectId;
-        this.objectInstanceId = objectInstanceId;
-        this.resourceId = resourceId;
-        this.resourceInstanceId = null;
+        this(objectId, objectInstanceId, resourceId, null);
+        validate();
     }
 
     /**
@@ -84,10 +75,8 @@ public class LwM2mPath {
      * @param resourceInstanceId the resource instance identifier
      */
     public LwM2mPath(int objectId, int objectInstanceId, int resourceId, int resourceInstanceId) {
-        this.objectId = objectId;
-        this.objectInstanceId = objectInstanceId;
-        this.resourceId = resourceId;
-        this.resourceInstanceId = resourceInstanceId;
+        this((Integer) objectId, (Integer) objectInstanceId, (Integer) resourceId, (Integer) resourceInstanceId);
+        validate();
     }
 
     /**
@@ -112,11 +101,28 @@ public class LwM2mPath {
             this.objectInstanceId = (p.length >= 2) ? Integer.valueOf(p[1]) : null;
             this.resourceId = (p.length >= 3) ? Integer.valueOf(p[2]) : null;
             this.resourceInstanceId = (p.length == 4) ? Integer.valueOf(p[3]) : null;
+            validate();
         } catch (NumberFormatException e) {
             throw new IllegalArgumentException("Invalid elements in path: " + path, e);
         }
     }
 
+    protected LwM2mPath(Integer objectId, Integer objectInstanceId, Integer resourceId, Integer resourceInstanceId) {
+        this.objectId = objectId;
+        this.objectInstanceId = objectInstanceId;
+        this.resourceId = resourceId;
+        this.resourceInstanceId = resourceInstanceId;
+    }
+
+    /**
+     * Validate the current path and raise {@link IllegalArgumentException} is path is not valid
+     * 
+     * @see LwM2mNodeUtil#validatePath(LwM2mPath)
+     */
+    protected void validate() {
+        LwM2mNodeUtil.validatePath(this);
+    }
+
     /**
      * @param path the end of the new path
      * @return a new path which is the concatenation of this path and the given one in parameter.
@@ -135,7 +141,18 @@ public class LwM2mPath {
      * @return a new path which is the concatenation of this path and the given one in parameter.
      */
     public LwM2mPath append(int end) {
-        return append(String.valueOf(end));
+        if (isRoot()) {
+            return new LwM2mPath(end);
+        } else if (isObject()) {
+            return new LwM2mPath(getObjectId(), end);
+        } else if (isObjectInstance()) {
+            return new LwM2mPath(getObjectId(), getObjectInstanceId(), end);
+        } else if (isResource()) {
+            return new LwM2mPath((int) getObjectId(), (int) getObjectInstanceId(), (int) getResourceId(), end);
+        } else {
+            throw new IllegalArgumentException(String.format(
+                    "Unable to append Id(%d) to path %s. Resource instance level is the deeper one.", end, this));
+        }
     }
 
     /**
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mSingleResource.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mSingleResource.java
index 907aa6be..8b0d3a55 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mSingleResource.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mSingleResource.java
@@ -21,7 +21,6 @@ import java.util.Map;
 import java.util.NoSuchElementException;
 
 import org.eclipse.leshan.core.model.ResourceModel.Type;
-import org.eclipse.leshan.util.Validate;
 
 /**
  * A resource with a single value.
@@ -35,7 +34,9 @@ public class LwM2mSingleResource implements LwM2mResource {
     private final Type type;
 
     protected LwM2mSingleResource(int id, Object value, Type type) {
-        Validate.notNull(value);
+        LwM2mNodeUtil.validateNotNull(value, "value MUST NOT be null");
+        LwM2mNodeUtil.validateResourceId(id);
+
         this.id = id;
         this.value = value;
         this.type = type;
@@ -46,34 +47,34 @@ public class LwM2mSingleResource implements LwM2mResource {
         switch (type) {
         case INTEGER:
             if (!(value instanceof Long))
-                throw new IllegalArgumentException(doesNotMatchMessage);
+                throw new LwM2mNodeException(doesNotMatchMessage);
             break;
         case FLOAT:
             if (!(value instanceof Double))
-                throw new IllegalArgumentException(doesNotMatchMessage);
+                throw new LwM2mNodeException(doesNotMatchMessage);
             break;
         case BOOLEAN:
             if (!(value instanceof Boolean))
-                throw new IllegalArgumentException(doesNotMatchMessage);
+                throw new LwM2mNodeException(doesNotMatchMessage);
             break;
         case OPAQUE:
             if (!(value instanceof byte[]))
-                throw new IllegalArgumentException(doesNotMatchMessage);
+                throw new LwM2mNodeException(doesNotMatchMessage);
             break;
         case STRING:
             if (!(value instanceof String))
-                throw new IllegalArgumentException(doesNotMatchMessage);
+                throw new LwM2mNodeException(doesNotMatchMessage);
             break;
         case TIME:
             if (!(value instanceof Date))
-                throw new IllegalArgumentException(doesNotMatchMessage);
+                throw new LwM2mNodeException(doesNotMatchMessage);
             break;
         case OBJLNK:
             if (!(value instanceof ObjectLink))
-                throw new IllegalArgumentException(doesNotMatchMessage);
+                throw new LwM2mNodeException(doesNotMatchMessage);
             break;
         default:
-            throw new IllegalArgumentException(String.format("Type %s is not supported", type.name()));
+            throw new LwM2mNodeException(String.format("Type %s is not supported", type.name()));
         }
         return new LwM2mSingleResource(id, value, type);
     }
@@ -192,8 +193,8 @@ public class LwM2mSingleResource implements LwM2mResource {
                 return false;
         } else {
             // Custom equals to handle byte arrays
-            return type == Type.OPAQUE ? Arrays.equals((byte[]) value, (byte[]) other.value) : value
-                    .equals(other.value);
+            return type == Type.OPAQUE ? Arrays.equals((byte[]) value, (byte[]) other.value)
+                    : value.equals(other.value);
         }
         return true;
     }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java
index f8da080b..5d208314 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java
@@ -32,6 +32,7 @@ import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.node.LwM2mMultipleResource;
 import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mNodeException;
 import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
@@ -64,7 +65,7 @@ public class LwM2mNodeJsonDecoder {
                 // return the most recent value
                 return (T) timestampedNodes.get(0).getNode();
             }
-        } catch (LwM2mJsonException e) {
+        } catch (LwM2mJsonException | LwM2mNodeException e) {
             throw new CodecException(e, "Unable to deserialize json [path:%s]", path);
         }
     }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvDecoder.java
index c1760178..638b1e4c 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvDecoder.java
@@ -23,8 +23,10 @@ import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectModel;
 import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.node.LwM2mIncompletePath;
 import org.eclipse.leshan.core.node.LwM2mMultipleResource;
 import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mNodeException;
 import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
@@ -48,7 +50,7 @@ public class LwM2mNodeTlvDecoder {
         try {
             Tlv[] tlvs = TlvDecoder.decode(ByteBuffer.wrap(content != null ? content : new byte[0]));
             return parseTlv(tlvs, path, model, nodeClass);
-        } catch (TlvException e) {
+        } catch (TlvException | LwM2mNodeException e) {
             throw new CodecException(String.format("Unable to decode tlv for path [%s]", path), e);
         }
     }
@@ -116,12 +118,13 @@ public class LwM2mNodeTlvDecoder {
                     // single instance object?
                     ObjectModel oModel = model.getObjectModel(path.getObjectId());
                     if (oModel != null && !oModel.multiple) {
-                        instanceId = 0;
+                        return (T) parseObjectInstanceTlv(tlvs, path.getObjectId(), 0, model);
                     } else {
-                        instanceId = LwM2mObjectInstance.UNDEFINED;
+                        return (T) parseObjectInstanceTlvWithoutId(tlvs, path.getObjectId(), model);
                     }
+                } else {
+                    return (T) parseObjectInstanceTlv(tlvs, path.getObjectId(), instanceId, model);
                 }
-                return (T) parseObjectInstanceTlv(tlvs, path.getObjectId(), instanceId, model);
             }
         }
 
@@ -166,7 +169,6 @@ public class LwM2mNodeTlvDecoder {
 
     private static LwM2mObjectInstance parseObjectInstanceTlv(Tlv[] rscTlvs, int objectId, int instanceId,
             LwM2mModel model) throws CodecException {
-        // read resources
         Map<Integer, LwM2mResource> resources = new HashMap<>(rscTlvs.length);
         for (Tlv rscTlv : rscTlvs) {
             LwM2mPath resourcePath = new LwM2mPath(objectId, instanceId, rscTlv.getIdentifier());
@@ -178,6 +180,22 @@ public class LwM2mNodeTlvDecoder {
             }
         }
         return new LwM2mObjectInstance(instanceId, resources.values());
+
+    }
+
+    private static LwM2mObjectInstance parseObjectInstanceTlvWithoutId(Tlv[] rscTlvs, int objectId, LwM2mModel model)
+            throws CodecException {
+        Map<Integer, LwM2mResource> resources = new HashMap<>(rscTlvs.length);
+        for (Tlv rscTlv : rscTlvs) {
+            LwM2mPath resourcePath = new LwM2mIncompletePath(objectId, rscTlv.getIdentifier());
+            LwM2mResource resource = parseResourceTlv(rscTlv, resourcePath, model);
+            LwM2mResource previousResource = resources.put(resource.getId(), resource);
+            if (previousResource != null) {
+                throw new CodecException("2 RESOURCE nodes (%s,%s) with the same identifier %d for path %s",
+                        previousResource, resource, resource.getId(), resourcePath);
+            }
+        }
+        return new LwM2mObjectInstance(resources.values());
     }
 
     private static LwM2mResource parseResourceTlv(Tlv tlv, LwM2mPath resourcePath, LwM2mModel model)
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvEncoder.java
index 43ba94d2..d8d641e8 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvEncoder.java
@@ -24,6 +24,7 @@ import java.util.Map.Entry;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.node.LwM2mIncompletePath;
 import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.node.LwM2mNodeVisitor;
 import org.eclipse.leshan.core.node.LwM2mObject;
@@ -103,8 +104,7 @@ public class LwM2mNodeTlvEncoder {
                 // the instanceId is part of the request path or is undefined
                 // so the instance TLV layer is not needed.
                 // encoded as an array of resource TLVs
-                tlvs = encodeResources(instance.getResources().values(),
-                        new LwM2mPath(path.getObjectId(), instance.getId()));
+                tlvs = encodeResources(instance.getResources().values(), new LwM2mIncompletePath(path.getObjectId()));
             } else {
                 // encoded as an instance TLV
                 Tlv[] resources = encodeResources(instance.getResources().values(),
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java
index e41372db..c37d4eb9 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java
@@ -127,9 +127,13 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor {
         coapRequest = Request.newPost();
         coapRequest.getOptions().setContentFormat(request.getContentFormat().getCode());
         // if no instance id, the client will assign it.
-        int instanceId = request.getInstanceId() != null ? request.getInstanceId() : LwM2mObjectInstance.UNDEFINED;
-        coapRequest.setPayload(encoder.encode(new LwM2mObjectInstance(instanceId, request.getResources()),
-                request.getContentFormat(), request.getPath(), model));
+        LwM2mObjectInstance instance;
+        if (request.getInstanceId() == null) {
+            instance = new LwM2mObjectInstance(request.getResources());
+        } else {
+            instance = new LwM2mObjectInstance(request.getInstanceId(), request.getResources());
+        }
+        coapRequest.setPayload(encoder.encode(instance, request.getContentFormat(), request.getPath(), model));
         setTarget(coapRequest, request.getPath());
     }
 


Test patch:
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/node/LwM2MResourceTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/node/LwM2MResourceTest.java
index 6132b0f5..9fc956a5 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/node/LwM2MResourceTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/node/LwM2MResourceTest.java
@@ -118,22 +118,22 @@ public class LwM2MResourceTest {
                 LwM2mMultipleResource.newBinaryResource(10, values2));
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test(expected = LwM2mNodeException.class)
     public void string_resource_with_null_value() {
         LwM2mSingleResource.newStringResource(1, null);
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test(expected = LwM2mNodeException.class)
     public void generic_resource_with_null_value() {
         LwM2mSingleResource.newResource(1, null, Type.INTEGER);
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test(expected = LwM2mNodeException.class)
     public void generic_instance_with_incompatible_value_and_type() {
         LwM2mSingleResource.newResource(0, "a string", Type.BOOLEAN);
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test(expected = LwM2mNodeException.class)
     public void integer_multi_instances_resource_with_null_value() {
         Map<Integer, Long> values = new HashMap<>();
         values.put(2, 2L);
@@ -141,7 +141,7 @@ public class LwM2MResourceTest {
         LwM2mMultipleResource.newIntegerResource(0, values);
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test(expected = LwM2mNodeException.class)
     public void generic_multi_instances_resource_with_null_value() {
         Map<Integer, String> values = new HashMap<>();
         values.put(2, "value");
@@ -149,7 +149,7 @@ public class LwM2MResourceTest {
         LwM2mMultipleResource.newResource(0, values, Type.STRING);
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test(expected = LwM2mNodeException.class)
     public void generic_multi_instance_with_incompatible_value_and_type() {
         Map<Integer, String> values = new HashMap<>();
         values.put(2, "value");
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 06eb19c2..5de0fee7 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
@@ -149,7 +149,7 @@ public class LwM2mNodeEncoderTest {
 
     @Test
     public void tlv_encode_device_object_instance_as_resources_array__undefined_instance_id() {
-        LwM2mObjectInstance oInstance = new LwM2mObjectInstance(LwM2mObjectInstance.UNDEFINED, getDeviceResources());
+        LwM2mObjectInstance oInstance = new LwM2mObjectInstance(getDeviceResources());
         byte[] encoded = encoder.encode(oInstance, ContentFormat.TLV, new LwM2mPath("/3"), model);
 
         Assert.assertArrayEquals(ENCODED_DEVICE_WITHOUT_INSTANCE, encoded);