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