diff --git a/leshan-core/pom.xml b/leshan-core/pom.xml
index 19d5d8a2e7..ede4a1bd5c 100644
--- a/leshan-core/pom.xml
+++ b/leshan-core/pom.xml
@@ -38,6 +38,10 @@ Contributors:
com.eclipsesource.minimal-json
minimal-json
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-cbor
+
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonDecoder.java
new file mode 100644
index 0000000000..be29f97925
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonDecoder.java
@@ -0,0 +1,328 @@
+/*******************************************************************************
+ * 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:
+ * Boya Zhang - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.node.codec.senml;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+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.LwM2mMultipleResource;
+import org.eclipse.leshan.core.node.LwM2mNode;
+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.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.codec.CodecException;
+import org.eclipse.leshan.senml.SenMLJson;
+import org.eclipse.leshan.senml.SenMLPack;
+import org.eclipse.leshan.senml.SenMLRecord;
+import org.eclipse.leshan.util.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LwM2mNodeSenMLJsonDecoder {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeSenMLJsonDecoder.class);
+
+ public static T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class nodeClass)
+ throws CodecException {
+ String jsonStrValue = content != null ? new String(content) : "";
+ SenMLPack pack = SenMLJson.fromSenMLJson(jsonStrValue);
+ return parseSenMLPack(pack, path, model, nodeClass);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static T parseSenMLPack(SenMLPack pack, LwM2mPath path, LwM2mModel model,
+ Class nodeClass) throws CodecException {
+
+ LOG.trace("Parsing SenML JSON object for path {}: {}", path, pack);
+
+ // Extract baseName
+ LwM2mPath baseName = extractAndValidateBaseName(pack, path);
+
+ // Group records by instance
+ Map> recordsByInstanceId = groupRecordsByInstanceId(pack.getRecords());
+
+ // Create lwm2m node
+ LwM2mNode node = null;
+ if (nodeClass == LwM2mObject.class) {
+ Collection instances = new ArrayList<>();
+ for (Entry> entryByInstanceId : recordsByInstanceId.entrySet()) {
+ Map resourcesMap = extractLwM2mResources(entryByInstanceId.getValue(), baseName,
+ model);
+
+ instances.add(new LwM2mObjectInstance(entryByInstanceId.getKey(), resourcesMap.values()));
+ }
+
+ node = new LwM2mObject(baseName.getObjectId(), instances);
+ } else if (nodeClass == LwM2mObjectInstance.class) {
+ // validate we have resources for only 1 instance
+ if (recordsByInstanceId.size() != 1)
+ throw new CodecException("One instance expected in the payload [path:%s]", path);
+
+ // Extract resources
+ Entry> instanceEntry = recordsByInstanceId.entrySet().iterator().next();
+ Map resourcesMap = extractLwM2mResources(instanceEntry.getValue(), baseName, model);
+
+ // Create instance
+ node = new LwM2mObjectInstance(instanceEntry.getKey(), resourcesMap.values());
+ } else if (nodeClass == LwM2mResource.class) {
+ // validate we have resources for only 1 instance
+ if (recordsByInstanceId.size() > 1)
+ throw new CodecException("Only one instance expected in the payload [path:%s]", path);
+
+ // Extract resources
+ Map resourcesMap = extractLwM2mResources(
+ recordsByInstanceId.values().iterator().next(), baseName, model);
+
+ // validate there is only 1 resource
+ if (resourcesMap.size() != 1)
+ throw new CodecException("One resource should be present in the payload [path:%s]", path);
+
+ node = resourcesMap.values().iterator().next();
+ } else {
+ throw new IllegalArgumentException("invalid node class: " + nodeClass);
+ }
+
+ return (T) node;
+ }
+
+ /**
+ * Extract and validate base name from SenML pack, and update name field with full path for each of SenML Record.
+ *
+ * @param pack
+ * @param requestPath
+ * @return
+ * @throws CodecException
+ */
+ private static LwM2mPath extractAndValidateBaseName(SenMLPack pack, LwM2mPath requestPath) throws CodecException {
+ String baseName = null;
+ for (SenMLRecord record : pack.getRecords()) {
+ if (record.getBaseName() != null && !record.getBaseName().isEmpty()) {
+ baseName = record.getBaseName();
+ break;
+ }
+ }
+
+ if (baseName != null && !baseName.isEmpty()) {
+ for (SenMLRecord record : pack.getRecords()) {
+ if (record.getName() != null && !record.getName().isEmpty()) {
+ record.setName(baseName + record.getName());
+ } else {
+ record.setName(baseName);
+ }
+ }
+ }
+
+ // Check baseName is valid
+ if (baseName != null && !baseName.isEmpty()) {
+ LwM2mPath bnPath = new LwM2mPath(baseName);
+
+ // check returned base name path is under requested path
+ if (requestPath.getObjectId() != null && bnPath.getObjectId() != null) {
+ if (!bnPath.getObjectId().equals(requestPath.getObjectId())) {
+ throw new CodecException("Basename path [%s] does not match requested path [%s].", bnPath,
+ requestPath);
+ }
+ if (requestPath.getObjectInstanceId() != null && bnPath.getObjectInstanceId() != null) {
+ if (!bnPath.getObjectInstanceId().equals(requestPath.getObjectInstanceId())) {
+ throw new CodecException("Basename path [%s] does not match requested path [%s].", bnPath,
+ requestPath);
+ }
+ if (requestPath.getResourceId() != null && bnPath.getResourceId() != null) {
+ if (!bnPath.getResourceId().equals(requestPath.getResourceId())) {
+ throw new CodecException("Basename path [%s] does not match requested path [%s].", bnPath,
+ requestPath);
+ }
+ }
+ }
+ }
+ return bnPath;
+ }
+ return null;
+ }
+
+ /**
+ * Group all SenML record by instanceId
+ *
+ * @param records
+ *
+ * @return a map (instanceId => collection of SenML Record)
+ */
+ private static Map> groupRecordsByInstanceId(Collection records)
+ throws CodecException {
+ Map> result = new HashMap<>();
+
+ for (SenMLRecord record : records) {
+ // Build resource path
+ LwM2mPath nodePath = new LwM2mPath(record.getName());
+
+ // Validate path
+ if (!nodePath.isResourceInstance() && !nodePath.isResource()) {
+ throw new CodecException(
+ "Invalid path [%s] for resource, it should be a resource or a resource instance path",
+ nodePath);
+ }
+
+ // Get SenML records for this instance
+ Collection recordForInstance = result.get(nodePath.getObjectInstanceId());
+ if (recordForInstance == null) {
+ recordForInstance = new ArrayList<>();
+ result.put(nodePath.getObjectInstanceId(), recordForInstance);
+ }
+
+ // Add it to the list
+ recordForInstance.add(record);
+ }
+
+ return result;
+ }
+
+ private static Map extractLwM2mResources(Collection records,
+ LwM2mPath baseName, LwM2mModel model) throws CodecException {
+ if (records == null)
+ return Collections.emptyMap();
+
+ // Extract LWM2M resources from JSON resource list
+ Map lwM2mResourceMap = new HashMap<>();
+ Map> multiResourceMap = new HashMap<>();
+
+ for (SenMLRecord record : records) {
+ // Build resource path
+ LwM2mPath nodePath = new LwM2mPath(record.getName());
+
+ // handle LWM2M resources
+ if (nodePath.isResourceInstance()) {
+ // Multi-instance resource
+ // Store multi-instance resource values in a map
+ // we will deal with it later
+ LwM2mPath resourcePath = new LwM2mPath(nodePath.getObjectId(), nodePath.getObjectInstanceId(),
+ nodePath.getResourceId());
+ Map multiResource = multiResourceMap.get(resourcePath);
+ if (multiResource == null) {
+ multiResource = new HashMap<>();
+ multiResourceMap.put(resourcePath, multiResource);
+ }
+ SenMLRecord previousResInstance = multiResource.put(nodePath.getResourceInstanceId(), record);
+ if (previousResInstance != null) {
+ throw new CodecException(
+ "2 RESOURCE_INSTANCE nodes (%s,%s) with the same identifier %d for path %s",
+ previousResInstance, record, nodePath.getResourceInstanceId(), nodePath);
+ }
+ } else if (nodePath.isResource()) {
+ // Single resource
+ Type expectedType = getResourceType(nodePath, model, record);
+ Object resourceValue = parseResourceValue(record.getResourceValue(), expectedType, nodePath);
+ LwM2mResource res = LwM2mSingleResource.newResource(nodePath.getResourceId(), resourceValue,
+ expectedType);
+ LwM2mResource previousRes = lwM2mResourceMap.put(nodePath.getResourceId(), res);
+ if (previousRes != null) {
+ throw new CodecException("2 RESOURCE nodes (%s,%s) with the same identifier %d for path %s",
+ previousRes, res, res.getId(), nodePath);
+ }
+ } else {
+ throw new CodecException(
+ "Invalid path [%s] for resource, it should be a resource or a resource instance path",
+ nodePath);
+ }
+ }
+
+ // Handle multiple resource instances.
+ for (Map.Entry> entry : multiResourceMap.entrySet()) {
+ LwM2mPath resourcePath = entry.getKey();
+ Map entries = entry.getValue();
+
+ if (entries != null && !entries.isEmpty()) {
+ Type expectedType = getResourceType(resourcePath, model, entries.values().iterator().next());
+ Map values = new HashMap<>();
+ for (Entry e : entries.entrySet()) {
+ Integer resourceInstanceId = e.getKey();
+ values.put(resourceInstanceId,
+ parseResourceValue(e.getValue().getResourceValue(), expectedType, resourcePath));
+ }
+ LwM2mResource resource = LwM2mMultipleResource.newResource(resourcePath.getResourceId(), values,
+ expectedType);
+ LwM2mResource previousRes = lwM2mResourceMap.put(resourcePath.getResourceId(), resource);
+ if (previousRes != null) {
+ throw new CodecException("2 RESOURCE nodes (%s,%s) with the same identifier %d for path %s",
+ previousRes, resource, resource.getId(), resourcePath);
+ }
+ }
+ }
+
+ // If we found nothing, we try to create an empty multi-instance resource
+ if (lwM2mResourceMap.isEmpty() && baseName.isResource()) {
+ ResourceModel resourceModel = model.getResourceModel(baseName.getObjectId(), baseName.getResourceId());
+ // We create it only if this respect the model
+ if (resourceModel == null || resourceModel.multiple) {
+ Type resourceType = getResourceType(baseName, model, null);
+ lwM2mResourceMap.put(baseName.getResourceId(), LwM2mMultipleResource
+ .newResource(baseName.getResourceId(), new HashMap(), resourceType));
+ }
+ }
+
+ return lwM2mResourceMap;
+ }
+
+ private static Object parseResourceValue(Object value, Type expectedType, LwM2mPath path) throws CodecException {
+ LOG.trace("Parse SenML JSON value for path {} and expected type {}: {}", path, expectedType, value);
+
+ try {
+ switch (expectedType) {
+ case INTEGER:
+ return ((Number) value).longValue();
+ case FLOAT:
+ return ((Number) value).doubleValue();
+ case BOOLEAN:
+ return value;
+ case TIME:
+ return new Date(((Number) value).longValue() * 1000L);
+ case OPAQUE:
+ return Base64.decodeBase64((String) value);
+ case STRING:
+ return value;
+ default:
+ throw new CodecException("Unsupported type %s for path %s", expectedType, path);
+ }
+ } catch (Exception e) {
+ throw new CodecException(e, "Invalid content [%s] for type %s for path %s", value, expectedType, path);
+ }
+ }
+
+ public static Type getResourceType(LwM2mPath rscPath, LwM2mModel model, SenMLRecord record) {
+ // Use model type in priority
+ ResourceModel rscDesc = model.getResourceModel(rscPath.getObjectId(), rscPath.getResourceId());
+ if (rscDesc != null && rscDesc.type != null)
+ return rscDesc.type;
+
+ // Then json type
+ if (record != null) {
+ Type type = record.getType();
+ if (type != null)
+ return type;
+ }
+
+ // Else use String as default
+ LOG.trace("unknown type for resource use string as default: {}", rscPath);
+ return Type.STRING;
+ }
+}
\ No newline at end of file
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java
index a072012dde..2de02ba7a5 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java
@@ -53,7 +53,7 @@ public static byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, Lw
SenMLPack pack = new SenMLPack();
pack.setRecords(internalEncoder.records);
- return SenMLJson.toJsonSenML(pack).getBytes();
+ return SenMLJson.toSenMLJson(pack).getBytes();
}
private static class InternalEncoder implements LwM2mNodeVisitor {
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCbor.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCbor.java
new file mode 100644
index 0000000000..6d483a1996
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCbor.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * 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:
+ * Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml;
+
+/**
+ * Helper for encoding/decoding SenML CBOR format
+ */
+public class SenMLCbor {
+ private static final SenMLCborPackSerDes serDes = new SenMLCborPackSerDes();
+
+ public static byte[] toSenMLCbor(SenMLPack pack) throws SenMLCborException {
+ return serDes.serializeToCbor(pack);
+ }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCborException.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCborException.java
new file mode 100644
index 0000000000..a5ecc9aff6
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCborException.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * 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:
+ * Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml;
+
+/**
+ * Exception thrown in case of Cbor encoding error
+ */
+public class SenMLCborException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public SenMLCborException(String message) {
+ super(message);
+ }
+
+ public SenMLCborException(String message, Exception cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCborPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCborPackSerDes.java
new file mode 100644
index 0000000000..6c7d7538b1
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCborPackSerDes.java
@@ -0,0 +1,119 @@
+/*******************************************************************************
+ * 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:
+ * Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml;
+
+import java.io.ByteArrayOutputStream;
+
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+
+import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
+import com.fasterxml.jackson.dataformat.cbor.CBORGenerator;
+
+public class SenMLCborPackSerDes {
+
+ public byte[] serializeToCbor(SenMLPack pack) throws SenMLCborException {
+ CBORFactory factory = new CBORFactory();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ try {
+ CBORGenerator generator = factory.createGenerator(out);
+ generator.writeStartArray(pack.getRecords().size());
+
+ for (SenMLRecord record : pack.getRecords()) {
+ boolean hasBaseName = false;
+ boolean hasBaseTime = false;
+ boolean hasName = false;
+ boolean hasTime = false;
+ int objectSize = 1;
+
+ if (record.getBaseName() != null && !record.getBaseName().isEmpty()) {
+ hasBaseName = true;
+ objectSize++;
+ }
+
+ if (record.getBaseTime() != null) {
+ hasBaseTime = true;
+ objectSize++;
+ }
+
+ if (record.getName() != null && !record.getName().isEmpty()) {
+ hasName = true;
+ objectSize++;
+ }
+
+ if (record.getTime() != null) {
+ hasTime = true;
+ objectSize++;
+ }
+
+ generator.writeStartObject(objectSize);
+
+ if (hasBaseName) {
+ generator.writeFieldId(-2);
+ generator.writeString(record.getBaseName());
+ }
+
+ if (hasBaseTime) {
+ generator.writeFieldId(-3);
+ generator.writeNumber(record.getBaseTime());
+ }
+
+ if (hasName) {
+ generator.writeFieldId(0);
+ generator.writeString(record.getName());
+ }
+
+ if (hasTime) {
+ generator.writeFieldId(6);
+ generator.writeNumber(record.getTime());
+ }
+
+ Type type = record.getType();
+ if (type != null) {
+ switch (record.getType()) {
+ case FLOAT:
+ generator.writeFieldId(2);
+ generator.writeNumber(record.getFloatValue().intValue());
+ break;
+ case BOOLEAN:
+ generator.writeFieldId(4);
+ generator.writeBoolean(record.getBooleanValue());
+ break;
+ case OBJLNK:
+ generator.writeStringField("vlo", record.getObjectLinkValue());
+ break;
+ case OPAQUE:
+ generator.writeFieldId(8);
+ generator.writeBinary(record.getOpaqueValue());
+ case STRING:
+ generator.writeFieldId(3);
+ generator.writeString(record.getStringValue());
+ break;
+ default:
+ break;
+ }
+ }
+ generator.writeEndObject();
+ }
+
+ generator.writeEndArray();
+ generator.close();
+ } catch (Exception ex) {
+ throw new SenMLCborException("Impossible to encode pack to CBOR: \n" + pack, ex);
+ }
+
+ return out.toByteArray();
+ }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJson.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJson.java
index 54fafa41d1..1170b02afe 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJson.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJson.java
@@ -20,13 +20,13 @@
* Helper for encoding/decoding SenML JSON format
*/
public class SenMLJson {
- private static final SenMLPackSerDes serDes = new SenMLPackSerDes();
+ private static final SenMLJsonPackSerDes serDes = new SenMLJsonPackSerDes();
- public static String toJsonSenML(SenMLPack pack) {
- return serDes.serialize(pack);
+ public static String toSenMLJson(SenMLPack pack) {
+ return serDes.serializeToJson(pack);
}
- public static SenMLPack fromJsonSenML(String jsonString) {
- return serDes.deserialize(Json.parse(jsonString).asArray());
+ public static SenMLPack fromSenMLJson(String jsonString) {
+ return serDes.deserializeFromJson(Json.parse(jsonString).asArray());
}
}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJsonPackSerDes.java
similarity index 96%
rename from leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLPackSerDes.java
rename to leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJsonPackSerDes.java
index 30828a03e4..a40c22ff90 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLPackSerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJsonPackSerDes.java
@@ -21,9 +21,9 @@
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;
-public class SenMLPackSerDes {
+public class SenMLJsonPackSerDes {
- public String serialize(SenMLPack pack) {
+ public String serializeToJson(SenMLPack pack) {
JsonArray jsonArray = new JsonArray();
for (SenMLRecord record : pack.getRecords()) {
@@ -73,7 +73,7 @@ public String serialize(SenMLPack pack) {
return jsonArray.toString();
}
- public SenMLPack deserialize(JsonArray array) {
+ public SenMLPack deserializeFromJson(JsonArray array) {
if (array == null)
return null;
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 355a0b4cde..e4e60a689a 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
@@ -37,6 +37,7 @@
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.node.ObjectLink;
import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
+import org.eclipse.leshan.core.node.codec.senml.LwM2mNodeSenMLJsonDecoder;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.tlv.Tlv;
import org.eclipse.leshan.tlv.Tlv.TlvType;
@@ -710,4 +711,86 @@ public void json_invalid_multi_resource_2_instances_with_the_same_id() {
decoder.decode(b.toString().getBytes(), ContentFormat.JSON, new LwM2mPath(3, 0, 11), model);
}
+
+ @Test
+ public void senml_json_decode_device_object_instance() {
+ StringBuilder payload = new StringBuilder();
+ payload.append("[{\"bn\":\"/3/0/\",\"n\":\"0\",\"vs\":\"Open Mobile Alliance\"},");
+ payload.append("{\"n\":\"1\",\"vs\":\"Lightweight M2M Client\"},");
+ payload.append("{\"n\":\"2\",\"vs\":\"345000123\"},");
+ payload.append("{\"n\":\"3\",\"vs\":\"1.0\"},");
+ payload.append("{\"n\":\"6/0\",\"v\":1},");
+ payload.append("{\"n\":\"6/1\",\"v\":5},");
+ payload.append("{\"n\":\"7/0\",\"v\":3800},");
+ payload.append("{\"n\":\"7/1\",\"v\":5000},");
+ payload.append("{\"n\":\"8/0\",\"v\":125},");
+ payload.append("{\"n\":\"8/1\",\"v\":900},");
+ payload.append("{\"n\":\"9\",\"v\":100},");
+ payload.append("{\"n\":\"10\",\"v\":15},");
+ payload.append("{\"n\":\"11/0\",\"v\":0},");
+ payload.append("{\"n\":\"13\",\"v\":1.3674912E9},");
+ payload.append("{\"n\":\"14\",\"vs\":\"+02:00\"},");
+ payload.append("{\"n\":\"16\",\"vs\":\"U\"}]");
+
+ LwM2mObjectInstance instance = LwM2mNodeSenMLJsonDecoder.decode(payload.toString().getBytes(), new LwM2mPath("/3/0"),
+ model, LwM2mObjectInstance.class);
+
+ assertNotNull(instance);
+ assertEquals(0, instance.getId());
+ assertEquals(instance.getResource(0).getValue(), "Open Mobile Alliance");
+ assertEquals(instance.getResource(1).getValue(), "Lightweight M2M Client");
+ assertEquals(instance.getResource(2).getValue(), "345000123");
+ assertEquals(instance.getResource(3).getValue(), "1.0");
+
+ assertEquals(instance.getResource(6).getValue(0), 1l);
+ assertEquals(instance.getResource(6).getValue(1), 5l);
+ assertEquals(instance.getResource(7).getValue(0), 3800l);
+ assertEquals(instance.getResource(7).getValue(1), 5000l);
+ assertEquals(instance.getResource(8).getValue(0), 125l);
+ assertEquals(instance.getResource(8).getValue(1), 900l);
+
+ assertEquals(instance.getResource(9).getValue(), 100l);
+ assertEquals(instance.getResource(10).getValue(), 15l);
+
+ assertEquals(instance.getResource(11).getValue(0), 0l);
+
+ Date time = (Date) instance.getResource(13).getValue();
+ assertEquals(time.getTime(), 1367491200000l);
+
+ assertEquals(instance.getResource(14).getValue(), "+02:00");
+ assertEquals(instance.getResource(16).getValue(), "U");
+ }
+
+ @Test
+ public void senml_json_decode_single_resource() {
+ String payload = "[{\"bn\":\"/3/0/0\",\"vs\":\"Open Mobile Alliance\"}]";
+ LwM2mResource resource = LwM2mNodeSenMLJsonDecoder.decode(payload.getBytes(), new LwM2mPath("/3/0/0"), model,
+ LwM2mResource.class);
+
+ assertNotNull(resource);
+ assertTrue(!resource.isMultiInstances());
+ assertEquals(0, resource.getId());
+ assertEquals(resource.getValue(), "Open Mobile Alliance");
+
+ payload = "[{\"bn\":\"/6/0/3\",\"v\":20.0}]";
+ resource = LwM2mNodeSenMLJsonDecoder.decode(payload.getBytes(), new LwM2mPath("/6/0/3"), model,
+ LwM2mResource.class);
+
+ assertNotNull(resource);
+ assertTrue(!resource.isMultiInstances());
+ assertEquals(3, resource.getId());
+ assertEquals(resource.getValue(), 20.0);
+ }
+
+ @Test
+ public void senml_json_decode_multiple_resource() {
+ String payload = "[{\"bn\":\"/3/0/7/\",\"n\":\"0\",\"v\":3800},{\"n\":\"1\",\"v\":5000}]";
+ LwM2mResource multipleResources = LwM2mNodeSenMLJsonDecoder.decode(payload.getBytes(), new LwM2mPath("/3/0/7"),
+ model, LwM2mResource.class);
+
+ assertNotNull(multipleResources);
+ assertTrue(multipleResources.isMultiInstances());
+ assertEquals(multipleResources.getValue(0), 3800l);
+ assertEquals(multipleResources.getValue(1), 5000l);
+ }
}
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/AbstractSenMLTest.java b/leshan-core/src/test/java/org/eclipse/leshan/senml/AbstractSenMLTest.java
new file mode 100644
index 0000000000..57fb5029eb
--- /dev/null
+++ b/leshan-core/src/test/java/org/eclipse/leshan/senml/AbstractSenMLTest.java
@@ -0,0 +1,88 @@
+/*******************************************************************************
+ * 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:
+ * Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml;
+
+public abstract class AbstractSenMLTest {
+ protected void givenResourceWithFloatValue(SenMLPack pack, String n, Number value) {
+ SenMLRecord elt = new SenMLRecord();
+ elt.setName(n);
+ elt.setFloatValue(value);
+ pack.addRecord(elt);
+ }
+
+ protected void givenResourceWithStringValue(SenMLPack pack, String n, String value) {
+ givenResourceWithStringValue(pack, null, n, value);
+ }
+
+ protected void givenResourceWithStringValue(SenMLPack pack, String bn, String n, String value) {
+ SenMLRecord elt = new SenMLRecord();
+ if (bn != null) {
+ elt.setBaseName(bn);
+ }
+
+ elt.setName(n);
+ elt.setStringValue(value);
+ pack.addRecord(elt);
+ }
+
+ /**
+ * Example of JSON payload request to Device Object of the LwM2M example client (Read /3/0)
+ *
+ * @return JSON payload
+ */
+ protected String givenSenMLJsonExample() {
+ StringBuilder b = new StringBuilder();
+ b.append("[{\"bn\":\"/3/0/\",\"n\":\"0\",\"vs\":\"Open Mobile Alliance\"},");
+ b.append("{\"n\":\"1\",\"vs\":\"Lightweight M2M Client\"},");
+ b.append("{\"n\":\"2\",\"vs\":\"345000123\"},");
+ b.append("{\"n\":\"3\",\"vs\":\"1.0\"},");
+ b.append("{\"n\":\"6/0\",\"v\":1},{\"n\":\"6/1\",\"v\":5},");
+ b.append("{\"n\":\"7/0\",\"v\":3800},{\"n\":\"7/1\",\"v\":5000},");
+ b.append("{\"n\":\"8/0\",\"v\":125},{\"n\":\"8/1\",\"v\":900},");
+ b.append("{\"n\":\"9\",\"v\":100},");
+ b.append("{\"n\":\"10\",\"v\":15},");
+ b.append("{\"n\":\"11/0\",\"v\":0},");
+ b.append("{\"n\":\"13\",\"v\":1.3674912E9},");
+ b.append("{\"n\":\"14\",\"vs\":\"+02:00\"},");
+ b.append("{\"n\":\"16\",\"vs\":\"U\"}]");
+ return b.toString();
+ }
+
+ protected SenMLPack givenDeviceObjectInstance() {
+ SenMLPack pack = new SenMLPack();
+
+ givenResourceWithStringValue(pack, "/3/0/", "0", "Open Mobile Alliance");
+ givenResourceWithStringValue(pack, "1", "Lightweight M2M Client");
+ givenResourceWithStringValue(pack, "2", "345000123");
+ givenResourceWithStringValue(pack, "3", "1.0");
+
+ givenResourceWithFloatValue(pack, "6/0", 1);
+ givenResourceWithFloatValue(pack, "6/1", 5);
+ givenResourceWithFloatValue(pack, "7/0", 3800);
+ givenResourceWithFloatValue(pack, "7/1", 5000);
+ givenResourceWithFloatValue(pack, "8/0", 125);
+ givenResourceWithFloatValue(pack, "8/1", 900);
+
+ givenResourceWithFloatValue(pack, "9", 100);
+ givenResourceWithFloatValue(pack, "10", 15);
+ givenResourceWithFloatValue(pack, "11/0", 0);
+ givenResourceWithFloatValue(pack, "13", 1367491215l);
+
+ givenResourceWithStringValue(pack, "14", "+02:00");
+ givenResourceWithStringValue(pack, "16", "U");
+
+ return pack;
+ }
+}
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLCborSerializerTest.java b/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLCborSerializerTest.java
new file mode 100644
index 0000000000..5cf9a057b9
--- /dev/null
+++ b/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLCborSerializerTest.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * 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:
+ * Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml;
+
+import org.eclipse.leshan.util.Hex;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SenMLCborSerializerTest extends AbstractSenMLTest {
+
+ @Test
+ public void serialize_device_object_to_senml_cbor() throws Exception {
+ byte[] cbor = SenMLCbor.toSenMLCbor(givenDeviceObjectInstance());
+
+ String expectedValue = "90a321652f332f302f00613003744f70656e204d6f62696c6520416c6c69616e6365a200613103764c696768747765696768"
+ + "74204d324d20436c69656e74a20061320369333435303030313233a20061330363312e30a20063362f300201a20063362f310205a20063372f"
+ + "3002190ed8a20063372f3102191388a20063382f3002187da20063382f3102190384a2006139021864a200623130020fa2006431312f300200"
+ + "a200623133021a5182428fa20062313403662b30323a3030a200623136036155";
+
+ Assert.assertTrue(expectedValue.equals(Hex.encodeHexString(cbor)));
+ }
+}
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLDeserializerTest.java b/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLDeserializerTest.java
deleted file mode 100644
index bf4970dd70..0000000000
--- a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLDeserializerTest.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*******************************************************************************
- * 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:
- * Boya Zhang - initial API and implementation
- *******************************************************************************/
-
-package org.eclipse.leshan.senml;
-
-import org.junit.Assert;
-import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class SenMLDeserializerTest {
-
- private Logger log = LoggerFactory.getLogger(getClass());
-
- @Test
- public void deserialize_device_object() {
- StringBuilder b = new StringBuilder();
- b.append("[{\"bn\":\"/3/0\",\"n\":\"0\",\"vs\":\"Open Mobile Alliance\"},");
- b.append("{\"n\":\"1\",\"vs\":\"Lightweight M2M Client\"},");
- b.append("{\"n\":\"2\",\"vs\":\"345000123\"},");
- b.append("{\"n\":\"6/0\",\"v\":1},{\"n\":\"6/1\",\"v\":5},");
- b.append("{\"n\":\"7/0\",\"v\":3800},{\"n\":\"7/1\",\"v\":5000},");
- b.append("{\"n\":\"8/0\",\"v\":125},{\"n\":\"8/1\",\"v\":900},");
- b.append("{\"n\":\"9\",\"v\":100},");
- b.append("{\"n\":\"10\",\"v\":15},");
- b.append("{\"n\":\"11/0\",\"v\":0},");
- b.append("{\"n\":\"13\",\"v\":1.3674912E9},");
- b.append("{\"n\":\"14\",\"vs\":\"+02:00\"},");
- b.append("{\"n\":\"15\",\"vs\":\"U\"}]");
-
- String dataString = b.toString();
- log.debug(dataString.trim());
- SenMLPack pack = SenMLJson.fromJsonSenML(dataString);
- log.debug(pack.toString());
-
- String outString = SenMLJson.toJsonSenML(pack);
- Assert.assertEquals(dataString.trim(), outString);
- }
-}
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonDeserializerTest.java b/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonDeserializerTest.java
new file mode 100644
index 0000000000..94b9ea1d11
--- /dev/null
+++ b/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonDeserializerTest.java
@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * 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:
+ * Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SenMLJsonDeserializerTest extends AbstractSenMLTest {
+
+ @Test
+ public void deserialize_device_object() {
+ String dataString = givenSenMLJsonExample();
+ SenMLPack pack = SenMLJson.fromSenMLJson(dataString);
+
+ String outString = SenMLJson.toSenMLJson(pack);
+ Assert.assertEquals(dataString.trim(), outString);
+ }
+}
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonSerializerTest.java b/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonSerializerTest.java
new file mode 100644
index 0000000000..66c64aacd9
--- /dev/null
+++ b/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonSerializerTest.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * 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:
+ * Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SenMLJsonSerializerTest extends AbstractSenMLTest {
+
+ @Test
+ public void serialize_device_object_to_senml_json() {
+ String json = SenMLJson.toSenMLJson(givenDeviceObjectInstance());
+ Assert.assertTrue(json.equals(givenSenMLJsonExample()));
+ }
+}
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLSerializerTest.java b/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLSerializerTest.java
deleted file mode 100644
index 6f2d3453b0..0000000000
--- a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLSerializerTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*******************************************************************************
- * 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:
- * Boya Zhang - initial API and implementation
- *******************************************************************************/
-
-package org.eclipse.leshan.senml;
-
-import org.junit.Assert;
-import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class SenMLSerializerTest {
-
- private Logger LOG = LoggerFactory.getLogger(getClass());
-
- @Test
- public void serialize_device_object() {
-
- SenMLPack pack = new SenMLPack();
-
- SenMLRecord elt1 = new SenMLRecord();
- elt1.setBaseName("/3/0");
- elt1.setName("0");
- elt1.setStringValue("Open Mobile Alliance");
- pack.addRecord(elt1);
-
- SenMLRecord elt2 = new SenMLRecord();
- elt2.setName("1");
- elt2.setStringValue("Lightweight M2M Client");
- pack.addRecord(elt2);
-
- SenMLRecord elt3 = new SenMLRecord();
- elt3.setName("2");
- elt3.setStringValue("345000123");
- pack.addRecord(elt3);
-
- SenMLRecord elt4 = new SenMLRecord();
- elt4.setName("6/0");
- elt4.setFloatValue(1);
- pack.addRecord(elt4);
-
- SenMLRecord elt5= new SenMLRecord();
- elt5.setName("6/1");
- elt5.setFloatValue(5);
- pack.addRecord(elt5);
-
- SenMLRecord elt6 = new SenMLRecord();
- elt6.setName("7/0");
- elt6.setFloatValue(3800);
- pack.addRecord(elt6);
-
- SenMLRecord elt7 = new SenMLRecord();
- elt7.setName("7/1");
- elt7.setFloatValue(5000);
- pack.addRecord(elt7);
-
- SenMLRecord elt8 = new SenMLRecord();
- elt8.setName("8/0");
- elt8.setFloatValue(125);
- pack.addRecord(elt8);
-
- SenMLRecord elt9 = new SenMLRecord();
- elt9.setName("8/1");
- elt9.setFloatValue(900);
- pack.addRecord(elt9);
-
- SenMLRecord elt10 = new SenMLRecord();
- elt10.setName("9");
- elt10.setFloatValue(100);
- pack.addRecord(elt10);
-
- SenMLRecord elt11 = new SenMLRecord();
- elt11.setName("10");
- elt11.setFloatValue(15);
- pack.addRecord(elt11);
-
- SenMLRecord elt12 = new SenMLRecord();
- elt12.setName("11/0");
- elt12.setFloatValue(0);
- pack.addRecord(elt12);
-
- SenMLRecord elt13= new SenMLRecord();
- elt13.setName("13");
- elt13.setFloatValue(1367491215l);
- pack.addRecord(elt13);
-
- SenMLRecord elt14 = new SenMLRecord();
- elt14.setName("14");
- elt14.setStringValue("+02:00");
- pack.addRecord(elt14);
-
- SenMLRecord elt15 = new SenMLRecord();
- elt15.setName("15");
- elt15.setStringValue("U");
- pack.addRecord(elt15);
-
- String json = SenMLJson.toJsonSenML(pack);
- LOG.debug("JSON String: " + json);
-
- StringBuilder b = new StringBuilder();
- b.append("[{\"bn\":\"/3/0\",\"n\":\"0\",\"vs\":\"Open Mobile Alliance\"},");
- b.append("{\"n\":\"1\",\"vs\":\"Lightweight M2M Client\"},");
- b.append("{\"n\":\"2\",\"vs\":\"345000123\"},");
- b.append("{\"n\":\"6/0\",\"v\":1},{\"n\":\"6/1\",\"v\":5},");
- b.append("{\"n\":\"7/0\",\"v\":3800},{\"n\":\"7/1\",\"v\":5000},");
- b.append("{\"n\":\"8/0\",\"v\":125},{\"n\":\"8/1\",\"v\":900},");
- b.append("{\"n\":\"9\",\"v\":100},");
- b.append("{\"n\":\"10\",\"v\":15},");
- b.append("{\"n\":\"11/0\",\"v\":0},");
- b.append("{\"n\":\"13\",\"v\":1.3674912E9},");
- b.append("{\"n\":\"14\",\"vs\":\"+02:00\"},");
- b.append("{\"n\":\"15\",\"vs\":\"U\"}]");
-
- Assert.assertTrue(json.equals(b.toString()));
- }
-}
diff --git a/pom.xml b/pom.xml
index 792d6dd853..baf8032d7c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -462,6 +462,11 @@ Contributors:
gson
2.2.4
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-cbor
+ 2.9.9
+
commons-lang
commons-lang