Repository: eclipse/leshan
Pull request #175 (2016-09-20 09:24:59)

Add object link support for TLV and Text format
PR based on  #161.


Patch:
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/DDFFileParser.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/DDFFileParser.java
index 7c427d6f..135354f0 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/model/DDFFileParser.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/DDFFileParser.java
@@ -168,6 +168,9 @@ public class DDFFileParser {
                 case "Time":
                     type = Type.TIME;
                     break;
+                case "Objlnk":
+                    type = Type.OBJLNK;
+                    break;
                 }
                 break;
             case "RangeEnumeration":
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/ResourceModel.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/ResourceModel.java
index 37580eb7..515a74b0 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/model/ResourceModel.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/ResourceModel.java
@@ -37,7 +37,7 @@ public class ResourceModel {
     }
 
     public enum Type {
-        STRING, INTEGER, FLOAT, BOOLEAN, OPAQUE, TIME
+        STRING, INTEGER, FLOAT, BOOLEAN, OPAQUE, TIME, OBJLNK
     }
 
     public final int id;
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 0284ead1..f9c28805 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
@@ -68,6 +68,9 @@ public class LwM2mMultipleResource implements LwM2mResource {
         case TIME:
             Validate.allElementsOfType(values.values(), Date.class);
             break;
+        case OBJLNK:
+            Validate.allElementsOfType(values.values(), ObjectLink.class);
+            break;
         default:
             throw new IllegalArgumentException(String.format("Type %s is not supported", type.name()));
         }
@@ -99,6 +102,11 @@ public class LwM2mMultipleResource implements LwM2mResource {
         return new LwM2mMultipleResource(id, values, Type.TIME);
     }
 
+    public static LwM2mMultipleResource newObjectLinkResource(int id, Map<Integer, ObjectLink> values) {
+        Validate.noNullElements(values.values());
+        return new LwM2mMultipleResource(id, values, Type.OBJLNK);
+    }
+
     public static LwM2mMultipleResource newBinaryResource(int id, Map<Integer, byte[]> values) {
         Validate.noNullElements(values.values());
         return new LwM2mMultipleResource(id, values, Type.OPAQUE);
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 2c4af802..e8f04660 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
@@ -61,6 +61,9 @@ public class LwM2mSingleResource implements LwM2mResource {
         case TIME:
             if (value instanceof Date)
                 break;
+        case OBJLNK:
+            if (value instanceof ObjectLink)
+                break;
             throw new IllegalArgumentException("Value does not match the given datatype");
         default:
             throw new IllegalArgumentException(String.format("Type %s is not supported", type.name()));
@@ -76,6 +79,10 @@ public class LwM2mSingleResource implements LwM2mResource {
         return new LwM2mSingleResource(id, value, Type.INTEGER);
     }
 
+    public static LwM2mSingleResource newObjectLinkResource(int id, ObjectLink objlink) {
+        return new LwM2mSingleResource(id, objlink, Type.OBJLNK);
+    }
+
     public static LwM2mSingleResource newBooleanResource(int id, boolean value) {
         return new LwM2mSingleResource(id, value, Type.BOOLEAN);
     }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/ObjectLink.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/ObjectLink.java
new file mode 100644
index 00000000..5ac22aaf
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/ObjectLink.java
@@ -0,0 +1,112 @@
+/*******************************************************************************
+ * Copyright (c) 2016 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 org.eclipse.leshan.util.Validate;
+
+/**
+ * The object link is used to refer an Instance of a given Object. An Object link value is composed of two concatenated
+ * 16-bits unsigned integers following the Network Byte Order convention. The Most Significant Halfword is an ObjectID,
+ * the Least Significant Hafword is an ObjectInstance ID. An Object Link referencing no Object Instance will contain the
+ * concatenation of 2 MAX-ID values (null link)
+ * 
+ * MAX-ID = 65535.
+ */
+public class ObjectLink {
+    private final static int MAXID = 65535;
+
+    private final int objectId;
+    private final int objectInstanceId;
+
+    /**
+     * Create a null link. An Object Link referencing no object instance
+     */
+    public ObjectLink() {
+        this(MAXID, MAXID);
+    }
+
+    /**
+     * Create a Object Link referencing an object instance with the objectId/objectInstanceId path.
+     */
+    public ObjectLink(int objectId, int objectInstanceId) {
+        // validate range
+        Validate.isTrue(0 <= objectId && objectId <= MAXID);
+        Validate.isTrue(0 <= objectInstanceId && objectInstanceId <= MAXID);
+
+        // validate null link
+        Validate.isTrue(
+                (objectId != MAXID && objectInstanceId != MAXID) || objectId == MAXID && objectInstanceId == MAXID);
+
+        this.objectId = objectId;
+        this.objectInstanceId = objectInstanceId;
+    }
+
+    /**
+     * Create a Object Link referencing an object instance with the given path.
+     */
+    public static ObjectLink fromPath(String path) {
+        LwM2mPath lwM2mPath = new LwM2mPath(path);
+        if (lwM2mPath.isRoot()) {
+            return new ObjectLink(); // create null link
+        } else if (lwM2mPath.isObjectInstance()) {
+            return new ObjectLink(lwM2mPath.getObjectId(), lwM2mPath.getObjectInstanceId());
+        } else {
+            throw new IllegalArgumentException("Invalid path: ObjectLink should reference an object instance");
+        }
+    }
+
+    public int getObjectId() {
+        return objectId;
+    }
+
+    public int getObjectInstanceId() {
+        return objectInstanceId;
+    }
+
+    public boolean isNullLink() {
+        return objectId == MAXID && objectInstanceId == MAXID;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + objectId;
+        result = prime * result + objectInstanceId;
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        ObjectLink other = (ObjectLink) obj;
+        if (objectId != other.objectId)
+            return false;
+        if (objectInstanceId != other.objectInstanceId)
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("/%d/%d", objectId, objectInstanceId);
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextDecoder.java
index b0d67cfd..8a87e07d 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextDecoder.java
@@ -23,6 +23,7 @@ import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.node.codec.InvalidValueException;
 import org.eclipse.leshan.util.Charsets;
 import org.eclipse.leshan.util.Validate;
@@ -79,6 +80,15 @@ public class LwM2mNodeTextDecoder {
             case TIME:
                 // number of seconds since 1970/1/1
                 return new Date(Long.valueOf(value) * 1000L);
+            case OBJLNK:
+                String[] intArr = value.split(":");
+                if (intArr.length != 2)
+                    throw new InvalidValueException("Invalid value for objectLink resource: " + value, path);
+                try {
+                    return new ObjectLink(Integer.parseInt(intArr[0]), Integer.parseInt(intArr[1]));
+                } catch (NumberFormatException e) {
+                    throw new InvalidValueException("Invalid value for objectLinkresource: " + value, path);
+                }
             case OPAQUE:
                 // not specified
             default:
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextEncoder.java
index d554cd75..a4b73066 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextEncoder.java
@@ -26,6 +26,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.node.codec.Lwm2mNodeEncoderUtil;
 import org.eclipse.leshan.util.Charsets;
 import org.eclipse.leshan.util.Validate;
@@ -91,6 +92,10 @@ public class LwM2mNodeTextEncoder {
                 // number of seconds since 1970/1/1
                 strValue = String.valueOf(((Date) val).getTime() / 1000L);
                 break;
+            case OBJLNK:
+                ObjectLink objlnk = (ObjectLink) val;
+                strValue = String.valueOf(objlnk.getObjectId() + ":" + objlnk.getObjectInstanceId());
+                break;
             default:
                 throw new IllegalArgumentException("Cannot encode " + val + " in text format");
             }
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 014d00a4..8fa76747 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
@@ -197,6 +197,8 @@ public class LwM2mNodeTlvDecoder {
                 return TlvDecoder.decodeDate(value);
             case OPAQUE:
                 return value;
+            case OBJLNK:
+                return TlvDecoder.decodeObjlnk(value);
             default:
                 throw new InvalidValueException("Unsupported type " + expectedType, path);
             }
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 daf041f9..d23d39e8 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
@@ -31,6 +31,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.node.codec.Lwm2mNodeEncoderUtil;
 import org.eclipse.leshan.tlv.Tlv;
 import org.eclipse.leshan.tlv.Tlv.TlvType;
@@ -181,6 +182,8 @@ public class LwM2mNodeTlvEncoder {
                 return TlvEncoder.encodeDate((Date) value);
             case OPAQUE:
                 return (byte[]) value;
+            case OBJLNK:
+                return TlvEncoder.encodeObjlnk((ObjectLink) value);
             default:
                 throw new IllegalArgumentException("Invalid value type: " + type);
             }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteRequest.java
index dd68ff72..6d89167d 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteRequest.java
@@ -26,6 +26,7 @@ import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.response.WriteResponse;
 import org.eclipse.leshan.util.Validate;
 
@@ -212,6 +213,23 @@ public class WriteRequest extends AbstractDownlinkRequest<WriteResponse> {
                 LwM2mSingleResource.newBinaryResource(resourceId, value));
     }
 
+    /**
+     * Request to write a <b> Objlnk Single-Instance Resource</b> using the TLV content format.
+     */
+    public WriteRequest(final int objectId, final int objectInstanceId, final int resourceId, ObjectLink value) {
+        this(ContentFormat.TLV, objectId, objectInstanceId, resourceId, value);
+    }
+
+    /**
+     * Request to write a <b> Objlnk Single-Instance Resource</b> using the given content format (OPAQUE, TLV, JSON,
+     * TEXT).
+     */
+    public WriteRequest(final ContentFormat contentFormat, final int objectId, final int objectInstanceId,
+            final int resourceId, ObjectLink value) {
+        this(Mode.REPLACE, contentFormat, new LwM2mPath(objectId, objectInstanceId, resourceId),
+                LwM2mSingleResource.newObjectLinkResource(resourceId, value));
+    }
+
     // ***************** write multi instance resource ****************** //
     /**
      * Request to write a <b>Multi-Instance Resource</b>.
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/response/ReadResponse.java b/leshan-core/src/main/java/org/eclipse/leshan/core/response/ReadResponse.java
index 7cfd6838..9371e041 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/response/ReadResponse.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/response/ReadResponse.java
@@ -23,6 +23,7 @@ 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.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.util.Validate;
 
 public class ReadResponse extends AbstractLwM2mResponse {
@@ -78,6 +79,11 @@ public class ReadResponse extends AbstractLwM2mResponse {
         return new ReadResponse(ResponseCode.CONTENT, LwM2mSingleResource.newIntegerResource(resourceId, value), null);
     }
 
+    public static ReadResponse success(int resourceId, ObjectLink value) {
+        return new ReadResponse(ResponseCode.CONTENT, LwM2mSingleResource.newObjectLinkResource(resourceId, value),
+                null);
+    }
+
     public static ReadResponse success(int resourceId, double value) {
         return new ReadResponse(ResponseCode.CONTENT, LwM2mSingleResource.newFloatResource(resourceId, value), null);
     }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/tlv/TlvDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/tlv/TlvDecoder.java
index 9cd6554f..a2c2f12f 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/tlv/TlvDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/tlv/TlvDecoder.java
@@ -24,6 +24,7 @@ import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 
+import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.tlv.Tlv.TlvType;
 import org.eclipse.leshan.util.Charsets;
 import org.eclipse.leshan.util.Hex;
@@ -184,6 +185,17 @@ public class TlvDecoder {
         }
     }
 
+    /**
+     * Decodes a byte array into a objlnk value.
+     */
+    public static ObjectLink decodeObjlnk(byte[] value) throws TlvException {
+        ByteBuffer bff = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN);
+        bff.put(value);
+        int val1 = bff.getShort(0);
+        int val2 = bff.getShort(2);
+        return new ObjectLink(val1, val2);
+    }
+
     /**
      * Decodes a byte array into an integer value.
      */
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/tlv/TlvEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/tlv/TlvEncoder.java
index ba7d4741..f02323a1 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/tlv/TlvEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/tlv/TlvEncoder.java
@@ -19,6 +19,7 @@ import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.Date;
 
+import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.util.Charsets;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -114,6 +115,16 @@ public class TlvEncoder {
         return tBuf.array();
     }
 
+    /**
+     * Encodes a Objlnk value.
+     */
+    public static byte[] encodeObjlnk(ObjectLink value) {
+        ByteBuffer objlnkBuffer = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN);
+        objlnkBuffer.putShort(0, (short) value.getObjectId());
+        objlnkBuffer.putShort(2, (short) value.getObjectInstanceId());
+        return objlnkBuffer.array();
+    }
+
     private static int tlvEncodedSize(Tlv tlv, int length) {
         int size = 1 /* HEADER */;
         size += (tlv.getIdentifier() < MAX_LENGTH_8BIT) ? 1 : 2; /* 8 bits or 16 bits identifiers */


Test patch:
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 27f5e190..cf6c576c 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
@@ -63,6 +63,8 @@ public class IntegrationTestHelper {
     public static final int FLOAT_RESOURCE_ID = 3;
     public static final int TIME_RESOURCE_ID = 4;
     public static final int OPAQUE_RESOURCE_ID = 5;
+    public static final int OBJLNK_MULTI_INSTANCE_RESOURCE_ID = 6;
+    public static final int OBJLNK_SINGLE_INSTANCE_RESOURCE_ID = 7;
 
     LeshanServer server;
     LwM2mClient client;
@@ -88,8 +90,12 @@ public class IntegrationTestHelper {
                 null, null, null);
         ResourceModel opaquefield = new ResourceModel(OPAQUE_RESOURCE_ID, "opaque", Operations.RW, false, false,
                 Type.OPAQUE, null, null, null);
+        ResourceModel objlnkfield = new ResourceModel(OBJLNK_MULTI_INSTANCE_RESOURCE_ID, "objlnk", Operations.RW, true, false, Type.OBJLNK, 
+                null, null, null);
+        ResourceModel objlnkSinglefield = new ResourceModel(OBJLNK_SINGLE_INSTANCE_RESOURCE_ID, "objlnk", Operations.RW, false, false, Type.OBJLNK,
+                null, null, null);
         objectModels.add(new ObjectModel(TEST_OBJECT_ID, "testobject", null, false, false, stringfield, booleanfield,
-                integerfield, floatfield, timefield, opaquefield));
+                integerfield, floatfield, timefield, opaquefield, objlnkfield, objlnkSinglefield));
 
         return objectModels;
     }
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/WriteTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/WriteTest.java
index 6468890a..a6795c43 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/WriteTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/WriteTest.java
@@ -31,11 +31,16 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
 import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
 
 import org.eclipse.leshan.ResponseCode;
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.node.LwM2mMultipleResource;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.ReadRequest;
 import org.eclipse.leshan.core.request.WriteRequest;
@@ -374,4 +379,76 @@ public class WriteTest {
         assertEquals(utcOffset, instance.getResource(14));
         assertEquals(timeZone, instance.getResource(15));
     }
+
+    @Test
+    public void can_write_multi_instance_objlnk_resource_in_tlv() throws InterruptedException {
+        Map<Integer, ObjectLink> neighbourCellReport = new HashMap<>();
+        neighbourCellReport.put(0, new ObjectLink(10245, 1));
+        neighbourCellReport.put(1, new ObjectLink(10242, 2));
+        neighbourCellReport.put(2, new ObjectLink(10244, 3));
+
+        // Write objlnk resource in TLV format
+        WriteResponse response = helper.server.send(helper.getClient(),
+                new WriteRequest(ContentFormat.TLV, IntegrationTestHelper.TEST_OBJECT_ID, 0,
+                        IntegrationTestHelper.OBJLNK_MULTI_INSTANCE_RESOURCE_ID, neighbourCellReport, Type.OBJLNK));
+
+        // Verify Write result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+
+        // Reading back the written OBJLNK value
+        ReadResponse readResponse = helper.server.send(helper.getClient(), new ReadRequest(
+                IntegrationTestHelper.TEST_OBJECT_ID, 0, IntegrationTestHelper.OBJLNK_MULTI_INSTANCE_RESOURCE_ID));
+        LwM2mMultipleResource resource = (LwM2mMultipleResource) readResponse.getContent();
+
+        // verify read value
+        assertEquals(((ObjectLink) resource.getValue(0)).getObjectId(), 10245);
+        assertEquals(((ObjectLink) resource.getValue(0)).getObjectInstanceId(), 1);
+        assertEquals(((ObjectLink) resource.getValue(1)).getObjectId(), 10242);
+        assertEquals(((ObjectLink) resource.getValue(1)).getObjectInstanceId(), 2);
+        assertEquals(((ObjectLink) resource.getValue(2)).getObjectId(), 10244);
+        assertEquals(((ObjectLink) resource.getValue(2)).getObjectInstanceId(), 3);
+    }
+
+    @Test
+    public void can_write_single_instance_objlnk_resource_in_tlv() throws InterruptedException {
+        ObjectLink data = new ObjectLink(10245, 1);
+
+        // Write objlnk resource in TLV format
+        WriteResponse response = helper.server.send(helper.getClient(),
+                new WriteRequest(ContentFormat.TLV, IntegrationTestHelper.TEST_OBJECT_ID, 0,
+                        IntegrationTestHelper.OBJLNK_SINGLE_INSTANCE_RESOURCE_ID, data));
+
+        // Verify Write result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+
+        // Reading back the written OBJLNK value
+        ReadResponse readResponse = helper.server.send(helper.getClient(), new ReadRequest(
+                IntegrationTestHelper.TEST_OBJECT_ID, 0, IntegrationTestHelper.OBJLNK_SINGLE_INSTANCE_RESOURCE_ID));
+        LwM2mSingleResource resource = (LwM2mSingleResource) readResponse.getContent();
+
+        // verify read value
+        assertEquals(((ObjectLink) resource.getValue()).getObjectId(), 10245);
+        assertEquals(((ObjectLink) resource.getValue()).getObjectInstanceId(), 1);
+    }
+
+    @Test
+    public void can_write_single_instance_objlnk_resource_in_text() throws InterruptedException {
+        // Write objlnk resource in TEXT format
+        WriteResponse response = helper.server.send(helper.getClient(),
+                new WriteRequest(ContentFormat.TEXT, IntegrationTestHelper.TEST_OBJECT_ID, 0,
+                        IntegrationTestHelper.OBJLNK_SINGLE_INSTANCE_RESOURCE_ID, new ObjectLink(10245, 0)));
+
+        // Verify Write result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+
+        // Reading back the written OBJLNK value
+        ReadResponse readResponse = helper.server.send(helper.getClient(), new ReadRequest(ContentFormat.TEXT,
+                IntegrationTestHelper.TEST_OBJECT_ID, 0, IntegrationTestHelper.OBJLNK_SINGLE_INSTANCE_RESOURCE_ID));
+        LwM2mSingleResource resource = (LwM2mSingleResource) readResponse.getContent();
+
+        System.out.println(resource.getValue().toString());
+        // verify read value
+        assertEquals(((ObjectLink) resource.getValue()).getObjectId(), 10245);
+        assertEquals(((ObjectLink) resource.getValue()).getObjectInstanceId(), 0);
+    }
 }