Repository: eclipse/leshan
Pull request #1194 (2022-01-06 17:00:04)

Some Fix and Improvement about SenML-CBOR number handling.
 - Fix a regression introduced when trying to be more strict about number format.
 - Rename some confusing API. (Floating naming was used to refer to a Number value)


Patch:
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/cbor/LwM2mNodeCborDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/cbor/LwM2mNodeCborDecoder.java
index e9e96475..6cf3bf7a 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/cbor/LwM2mNodeCborDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/cbor/LwM2mNodeCborDecoder.java
@@ -93,8 +93,13 @@ public class LwM2mNodeCborDecoder implements NodeDecoder {
         case Integer:
             if (cborObject.HasTag(1))
                 return Type.TIME;
-            else
-                return Type.FLOAT;
+            else {
+                if (cborObject.CanValueFitInInt64())
+                    return Type.INTEGER;
+                else {
+                    return Type.UNSIGNED_INTEGER;
+                }
+            }
         case TextString:
             if (cborObject.HasTag(0))
                 return Type.TIME;
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java
index ddd67f73..d269506e 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java
@@ -13,6 +13,8 @@
  *******************************************************************************/
 package org.eclipse.leshan.core.node.codec.senml;
 
+import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -513,9 +515,11 @@ public class LwM2mNodeSenMLDecoder implements TimestampedNodeDecoder, MultiNodeD
         if (rscDesc != null && rscDesc.type != null)
             return rscDesc.type;
 
-        // Then json type
+        // TODO not sure that guessing type is a good idea...
+
+        // Then senml type
         if (record != null) {
-            Type type = record.getType();
+            Type type = guessTypeFromRecord(record);
             if (type != null)
                 return type;
         }
@@ -525,6 +529,40 @@ public class LwM2mNodeSenMLDecoder implements TimestampedNodeDecoder, MultiNodeD
         return Type.STRING;
     }
 
+    private Type guessTypeFromRecord(SenMLRecord record) {
+        SenMLRecord.Type type = record.getType();
+
+        switch (type) {
+        case STRING:
+            return Type.STRING;
+        case OPAQUE:
+            return Type.OPAQUE;
+        case BOOLEAN:
+            return Type.BOOLEAN;
+        case OBJLNK:
+            return Type.OBJLNK;
+        case NUMBER:
+            Number numberValue = record.getNumberValue();
+            if (numberValue instanceof ULong) {
+                return Type.UNSIGNED_INTEGER;
+            } else if (numberValue instanceof BigInteger) {
+                if (((BigInteger) numberValue).signum() <= 0) {
+                    return Type.INTEGER;
+                } else {
+                    return Type.UNSIGNED_INTEGER;
+                }
+            } else if (numberValue instanceof Byte || numberValue instanceof Short || numberValue instanceof Integer
+                    || numberValue instanceof Long) {
+                return Type.INTEGER;
+            } else if (numberValue instanceof Float || numberValue instanceof Double
+                    || numberValue instanceof BigDecimal) {
+                return Type.FLOAT;
+            }
+        default:
+            return null;
+        }
+    }
+
     protected Long numberToLong(Number number, boolean permissiveNumberConvertion) {
         return NumberUtil.numberToLong(number, permissiveNumberConvertion);
     }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java
index 376a086c..34a9cd39 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLEncoder.java
@@ -285,13 +285,13 @@ public class LwM2mNodeSenMLEncoder implements TimestampedNodeEncoder, MultiNodeE
             case INTEGER:
             case UNSIGNED_INTEGER:
             case FLOAT:
-                record.setFloatValue((Number) value);
+                record.setNumberValue((Number) value);
                 break;
             case BOOLEAN:
                 record.setBooleanValue((Boolean) value);
                 break;
             case TIME:
-                record.setFloatValue((((Date) value).getTime() / 1000L));
+                record.setNumberValue((((Date) value).getTime() / 1000L));
                 break;
             case OPAQUE:
                 record.setOpaqueValue((byte[]) value);
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/util/datatype/NumberUtil.java b/leshan-core/src/main/java/org/eclipse/leshan/core/util/datatype/NumberUtil.java
index de9d7185..435020d2 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/util/datatype/NumberUtil.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/util/datatype/NumberUtil.java
@@ -191,7 +191,7 @@ public class NumberUtil {
         if (number instanceof Float || number instanceof Double || number instanceof BigDecimal)
             return number.doubleValue();
 
-        throw new IllegalArgumentException(String.format("Floating-point number expected but was %s", number,
+        throw new IllegalArgumentException(String.format("Floating-point number expected but was %s (%s)", number,
                 number.getClass().getCanonicalName()));
     }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLRecord.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLRecord.java
index 8a49cf19..5674a20f 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLRecord.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLRecord.java
@@ -16,8 +16,6 @@ package org.eclipse.leshan.senml;
 
 import java.util.Arrays;
 
-import org.eclipse.leshan.core.model.ResourceModel;
-import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.util.Hex;
 
 /**
@@ -27,24 +25,28 @@ import org.eclipse.leshan.core.util.Hex;
  */
 public class SenMLRecord {
 
+    public enum Type {
+        STRING, NUMBER, BOOLEAN, OPAQUE, OBJLNK
+    }
+
     private String baseName = null;
     private Long baseTime;
 
     private String name;
     private Long time;
 
-    private Number floatValue;
+    private Number numberValue;
     private Boolean booleanValue;
     private String objectLinkValue;
     private String stringValue;
     private byte[] opaqueValue;
 
-    public ResourceModel.Type getType() {
+    public Type getType() {
         if (booleanValue != null) {
             return Type.BOOLEAN;
         }
-        if (floatValue != null) {
-            return Type.FLOAT;
+        if (numberValue != null) {
+            return Type.NUMBER;
         }
         if (objectLinkValue != null) {
             return Type.OBJLNK;
@@ -74,12 +76,12 @@ public class SenMLRecord {
         this.name = name;
     }
 
-    public Number getFloatValue() {
-        return floatValue;
+    public Number getNumberValue() {
+        return numberValue;
     }
 
-    public void setFloatValue(Number floatValue) {
-        this.floatValue = floatValue;
+    public void setNumberValue(Number numberValue) {
+        this.numberValue = numberValue;
     }
 
     public Boolean getBooleanValue() {
@@ -134,8 +136,8 @@ public class SenMLRecord {
         if (booleanValue != null) {
             return booleanValue;
         }
-        if (floatValue != null) {
-            return floatValue;
+        if (numberValue != null) {
+            return numberValue;
         }
         if (objectLinkValue != null) {
             return objectLinkValue;
@@ -156,7 +158,7 @@ public class SenMLRecord {
         result = prime * result + ((baseName == null) ? 0 : baseName.hashCode());
         result = prime * result + ((baseTime == null) ? 0 : baseTime.hashCode());
         result = prime * result + ((booleanValue == null) ? 0 : booleanValue.hashCode());
-        result = prime * result + ((floatValue == null) ? 0 : floatValue.hashCode());
+        result = prime * result + ((numberValue == null) ? 0 : numberValue.hashCode());
         result = prime * result + ((name == null) ? 0 : name.hashCode());
         result = prime * result + ((objectLinkValue == null) ? 0 : objectLinkValue.hashCode());
         result = prime * result + ((stringValue == null) ? 0 : stringValue.hashCode());
@@ -201,10 +203,10 @@ public class SenMLRecord {
                 return false;
         } else if (!booleanValue.equals(other.booleanValue))
             return false;
-        if (floatValue == null) {
-            if (other.floatValue != null)
+        if (numberValue == null) {
+            if (other.numberValue != null)
                 return false;
-        } else if (!floatValue.equals(other.floatValue))
+        } else if (!numberValue.equals(other.numberValue))
             return false;
         if (name == null) {
             if (other.name != null)
@@ -233,8 +235,8 @@ public class SenMLRecord {
     @Override
     public String toString() {
         return String.format(
-                "SenMLRecord [baseName=%s, baseTime=%d, name=%s, time=%d, floatValue=%s, booleanValue=%s, objectLinkValue=%s, stringValue=%s, opaque=%s]",
-                baseName, baseTime, name, time, floatValue, booleanValue, objectLinkValue, stringValue,
+                "SenMLRecord [baseName=%s, baseTime=%d, name=%s, time=%d, numberValue=%s, booleanValue=%s, objectLinkValue=%s, stringValue=%s, opaque=%s]",
+                baseName, baseTime, name, time, numberValue, booleanValue, objectLinkValue, stringValue,
                 opaqueValue != null ? Hex.encodeHexString(opaqueValue) : "null");
     }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java
index ef30394e..2633d740 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java
@@ -22,12 +22,12 @@ import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.util.Collection;
 
-import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.util.datatype.NumberUtil;
 import org.eclipse.leshan.core.util.datatype.ULong;
 import org.eclipse.leshan.senml.SenMLException;
 import org.eclipse.leshan.senml.SenMLPack;
 import org.eclipse.leshan.senml.SenMLRecord;
+import org.eclipse.leshan.senml.SenMLRecord.Type;
 
 import com.upokecenter.cbor.CBORNumber;
 import com.upokecenter.cbor.CBORObject;
@@ -80,28 +80,38 @@ public class SenMLCborPackSerDes {
                 boolean hasValue = false;
                 if (v != null && v.isNumber()) {
                     CBORNumber number = v.AsNumber();
-                    if (number.IsInteger()) {
+                    switch (number.getKind()) {
+                    case Integer:
+                    case EInteger:
                         if (number.IsNegative()) {
                             if (number.CanFitInInt64()) {
-                                record.setFloatValue(number.ToInt64Unchecked());
+                                record.setNumberValue(number.ToInt64Unchecked());
                             } else {
-                                record.setFloatValue((BigInteger) v.ToObject(BigInteger.class));
+                                record.setNumberValue((BigInteger) v.ToObject(BigInteger.class));
                             }
                         } else {
                             if (number.CanFitInInt64()) {
-                                record.setFloatValue(number.ToInt64Unchecked());
+                                record.setNumberValue(number.ToInt64Unchecked());
                             } else if (number.ToEIntegerIfExact().GetSignedBitLengthAsInt64() == 64) {
-                                record.setFloatValue(ULong.valueOf(number.ToInt64Unchecked()));
+                                record.setNumberValue(ULong.valueOf(number.ToInt64Unchecked()));
                             } else {
-                                record.setFloatValue((BigInteger) v.ToObject(BigInteger.class));
+                                record.setNumberValue((BigInteger) v.ToObject(BigInteger.class));
                             }
                         }
-                    } else {
+                        break;
+                    case Double:
+                    case EFloat:
+                    case EDecimal:
                         if (v.AsNumber().CanFitInDouble()) {
-                            record.setFloatValue(v.AsDoubleValue());
+                            record.setNumberValue(v.AsDoubleValue());
                         } else {
-                            record.setFloatValue((BigDecimal) v.ToObject(BigDecimal.class));
+                            record.setNumberValue((BigDecimal) v.ToObject(BigDecimal.class));
                         }
+                        break;
+                    default:
+                        throw new SenMLException(
+                                "Invalid SenML record : unexpected kind of number %s is not supported in %s",
+                                number.getKind(), o);
                     }
                     hasValue = true;
                 }
@@ -167,8 +177,8 @@ public class SenMLCborPackSerDes {
                 Type type = record.getType();
                 if (type != null) {
                     switch (record.getType()) {
-                    case FLOAT:
-                        Number value = record.getFloatValue();
+                    case NUMBER:
+                        Number value = record.getNumberValue();
                         if (value instanceof Byte) {
                             cborRecord.Add(2, value.byteValue());
                         } else if (value instanceof Short) {
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonRecordSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonRecordSerDes.java
index 8ed50c45..e414ec0c 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonRecordSerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonRecordSerDes.java
@@ -17,12 +17,12 @@ package org.eclipse.leshan.senml.json.jackson;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 
-import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.util.Base64;
 import org.eclipse.leshan.core.util.datatype.ULong;
 import org.eclipse.leshan.core.util.json.JacksonJsonSerDes;
 import org.eclipse.leshan.core.util.json.JsonException;
 import org.eclipse.leshan.senml.SenMLRecord;
+import org.eclipse.leshan.senml.SenMLRecord.Type;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
@@ -71,8 +71,8 @@ public class SenMLJsonRecordSerDes extends JacksonJsonSerDes<SenMLRecord> {
         Type type = record.getType();
         if (type != null) {
             switch (record.getType()) {
-            case FLOAT:
-                Number value = record.getFloatValue();
+            case NUMBER:
+                Number value = record.getNumberValue();
                 // integer
                 if (value instanceof Byte) {
                     jsonObj.put("v", value.byteValue());
@@ -147,7 +147,7 @@ public class SenMLJsonRecordSerDes extends JacksonJsonSerDes<SenMLRecord> {
         JsonNode v = o.get("v");
         boolean hasValue = false;
         if (v != null && v.isNumber()) {
-            record.setFloatValue(v.numberValue());
+            record.setNumberValue(v.numberValue());
             hasValue = true;
         }
 


Test patch:
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 11ed0a16..4e47e396 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
@@ -1063,7 +1063,7 @@ public class LwM2mNodeDecoderTest {
                 ((LwM2mObject) timestampedResources.get(1).getNode()).getInstance(0).getResource(1).getValue());
 
         assertEquals(Long.valueOf(268600000 - 50), timestampedResources.get(2).getTimestamp());
-        assertEquals(23.0d,
+        assertEquals(23l,
                 ((LwM2mObject) timestampedResources.get(2).getNode()).getInstance(1).getResource(1).getValue());
         assertEquals("a string",
                 ((LwM2mObject) timestampedResources.get(2).getNode()).getInstance(0).getResource(0).getValue());
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java
index 2a7cb3f7..9346152a 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java
@@ -22,7 +22,7 @@ public abstract class AbstractSenMLTest {
     private void givenResourceWithFloatValue(SenMLPack pack, String n, Number value) {
         SenMLRecord elt = new SenMLRecord();
         elt.setName(n);
-        elt.setFloatValue(value);
+        elt.setNumberValue(value);
         pack.addRecord(elt);
     }
 
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java
index df89b06d..2dd89439 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java
@@ -14,6 +14,8 @@
 
 package org.eclipse.leshan.core.senml.cbor;
 
+import static org.junit.Assert.assertEquals;
+
 import java.util.Arrays;
 import java.util.Collection;
 
@@ -22,6 +24,7 @@ import org.eclipse.leshan.senml.SenMLDecoder;
 import org.eclipse.leshan.senml.SenMLEncoder;
 import org.eclipse.leshan.senml.SenMLException;
 import org.eclipse.leshan.senml.SenMLPack;
+import org.eclipse.leshan.senml.SenMLRecord;
 import org.eclipse.leshan.senml.cbor.upokecenter.SenMLCborUpokecenterEncoderDecoder;
 import org.junit.Assert;
 import org.junit.Test;
@@ -67,7 +70,7 @@ public class SenMLCborSerializerTest extends AbstractSenMLTest {
     }
 
     @Test
-    public void deserialize_opaque_object_() throws Exception {
+    public void deserialize_opaque_resource() throws Exception {
         // value : [{-2: "/0/0/3", 8: h'ABCDEF'}]
         byte[] cbor = Hex.decodeHex("81a221662f302f302f330843abcdef".toCharArray());
         SenMLPack pack = decoder.fromSenML(cbor);
@@ -77,7 +80,7 @@ public class SenMLCborSerializerTest extends AbstractSenMLTest {
     }
 
     @Test
-    public void serialize_opaque_object_() throws Exception {
+    public void serialize_opaque_resource_() throws Exception {
         SenMLPack pack = getPackWithSingleOpaqueValue("/0/0/3", Hex.decodeHex("ABCDEF".toCharArray()));
         byte[] cbor = encoder.toSenML(pack);
 
@@ -85,4 +88,16 @@ public class SenMLCborSerializerTest extends AbstractSenMLTest {
         String expected = "81a221662f302f302f330843abcdef";
         Assert.assertEquals(expected, Hex.encodeHexString(cbor));
     }
+
+    @Test
+    public void deserialize_float_resource() throws Exception {
+        // value : [{2: 300.0, -2: "/3442/0/130"}]
+        byte[] cbor = Hex.decodeHex("81a202f95cb0216b2f333434322f302f313330".toCharArray());
+        SenMLPack pack = decoder.fromSenML(cbor);
+
+        assertEquals(pack.getRecords().size(), 1);
+        SenMLRecord record = pack.getRecords().get(0);
+        assertEquals("/3442/0/130", record.getBaseName());
+        assertEquals(300.0d, record.getNumberValue());
+    }
 }
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLTestUtil.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLTestUtil.java
index f6cc0bc5..0cb16467 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLTestUtil.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLTestUtil.java
@@ -48,9 +48,9 @@ public class SenMLTestUtil {
         assertFieldEquals("opaque value", expected.getOpaqueValue(), actual.getOpaqueValue());
         assertFieldEquals("string value", expected.getStringValue(), actual.getStringValue());
 
-        if (!equals(expected.getFloatValue(), actual.getFloatValue())) {
-            Assert.fail(String.format("Records not equals :float value differ expected %s, actual %s",
-                    expected.getFloatValue(), actual.getFloatValue()));
+        if (!equals(expected.getNumberValue(), actual.getNumberValue())) {
+            Assert.fail(String.format("Records not equals :number value differ expected %s, actual %s",
+                    expected.getNumberValue(), actual.getNumberValue()));
         }
     }