diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java index ce0a3a98e4..e50b6faefa 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java @@ -92,6 +92,9 @@ public T decode(byte[] content, LwM2mPath path, LwM2mModel if (timestampedNodes.size() == 0) { return null; } else { + TimestampedLwM2mNode timestampedNode = timestampedNodes.get(0); + validateNoTimestampedValue(timestampedNode, path); + // return the most recent value return (T) timestampedNodes.get(0).getNode(); } @@ -112,6 +115,12 @@ public List decodeTimestampedData(byte[] content, LwM2mPat } } + protected void validateNoTimestampedValue(TimestampedLwM2mNode timestampedNode, LwM2mPath path) { + if (timestampedNode.getTimestamp() != null) { + throw new CodecException("Unable to decode node[path:%s] : value should not be timestamped", path); + } + } + private List parseJSON(JsonRootObject jsonObject, LwM2mPath requestPath, LwM2mModel model, Class nodeClass) throws CodecException { @@ -233,7 +242,9 @@ private BigDecimal computeTimestamp(BigDecimal baseTime, BigDecimal time) { /** * Group all JsonArrayEntry by time-stamp * - * @return a map (relativeTimestamp => collection of JsonArrayEntry) + * @return a sorted map (relativeTimestamp => collection of JsonArrayEntry) order by descending time-stamp (most + * recent one at first place). If null time-stamp (meaning no time information) exists it always at first + * place. */ private SortedMap> groupJsonEntryByTimestamp(JsonRootObject jsonObject) { SortedMap> result = new TreeMap<>(new Comparator() { @@ -243,9 +254,9 @@ public int compare(BigDecimal o1, BigDecimal o2) { // - supports null (time null means 0 if there is a base time) // - reverses natural order (most recent value in first) if (o1 == null) { - return o2 == null ? 0 : 1; + return o2 == null ? 0 : -1; } else if (o2 == null) { - return -1; + return 1; } else { return o2.compareTo(o1); } 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 f07e42a451..972a129605 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 @@ -104,9 +104,7 @@ public T decode(byte[] content, LwM2mPath path, LwM2mModel throw new CodecException("Invalid path [%s] for resource, it should start by %s", resolvedRecord.getPath(), path); } - if (resolvedRecord.getTimeStamp() != null) { - throw new CodecException("Unable to decode node[path:%s] : value should not be timestamped", path); - } + validateNoTimestampedRecord(resolvedRecord); resolvedRecords.add(resolvedRecord); } @@ -141,6 +139,7 @@ public Map decodeNodes(byte[] content, List pat // Meaning that a given path could have no corresponding value. nodes.put(path, null); } else { + validateNoTimestampedRecord(records); LwM2mNode node = parseRecords(recordsByPath.get(path), path, model, DefaultLwM2mDecoder.nodeClassFromPath(path)); nodes.put(path, node); @@ -153,6 +152,7 @@ public Map decodeNodes(byte[] content, List pat for (SenMLRecord record : pack.getRecords()) { LwM2mResolvedSenMLRecord resolvedRecord = resolver.resolve(record); LwM2mPath path = resolvedRecord.getPath(); + validateNoTimestampedRecord(resolvedRecord); LwM2mNode node = parseRecords(Arrays.asList(resolvedRecord), path, model, DefaultLwM2mDecoder.nodeClassFromPath(path)); nodes.put(path, node); @@ -165,6 +165,19 @@ public Map decodeNodes(byte[] content, List pat } } + protected void validateNoTimestampedRecord(Collection resolvedRecords) { + for (LwM2mResolvedSenMLRecord resolvedRecord : resolvedRecords) { + validateNoTimestampedRecord(resolvedRecord); + } + } + + protected void validateNoTimestampedRecord(LwM2mResolvedSenMLRecord resolvedRecord) { + if (resolvedRecord.getTimeStamp() != null) { + throw new CodecException("Unable to decode node[path:%s] : value should not be timestamped", + resolvedRecord.getPath()); + } + } + @Override public List decodeTimestampedData(byte[] content, LwM2mPath path, LwM2mModel model, Class nodeClass) throws CodecException { @@ -460,9 +473,9 @@ private SortedMap> groupRecordB public int compare(BigDecimal o1, BigDecimal o2) { // null at first place if (o1 == null) { - return o2 == null ? 0 : 1; + return o2 == null ? 0 : -1; } else if (o2 == null) { - return -1; + return 1; } else { return o2.compareTo(o1); } 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 daa3bdf93a..0bc63b5b5a 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 @@ -136,7 +136,8 @@ public byte[] encodeTimestampedData(List timestampedNodes, SenMLPack pack = new SenMLPack(); for (TimestampedLwM2mNode timestampedLwM2mNode : timestampedNodes) { - if (timestampedLwM2mNode.getTimestamp().getEpochSecond() < 268_435_456) { + if (timestampedLwM2mNode.isTimestamped() + && timestampedLwM2mNode.getTimestamp().getEpochSecond() < 268_435_456) { // The smallest absolute Time value that can be expressed (2**28) is 1978-07-04 21:24:16 UTC. // see https://tools.ietf.org/html/rfc8428#section-4.5.3 throw new CodecException( diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveResponse.java b/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveResponse.java index 2954b4d206..3cafcc1497 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveResponse.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveResponse.java @@ -85,13 +85,20 @@ public LwM2mChildNode getContent() { */ @Override public TimestampedLwM2mNode getTimestampedLwM2mNode() { - return super.getTimestampedLwM2mNode(); + if (timestampedValues != null && !timestampedValues.isEmpty()) { + return timestampedValues.get(0); + } else { + return super.getTimestampedLwM2mNode(); + } } /** * A list of {@link LwM2mNode} representing different state of this resources at different instant. This method * returns value only on notification when client are using "Notification Storing When Disabled or Offline" and * content format support it. + *

+ * The list is sorted by descending time-stamp order (most recent one at first place). If null time-stamp (meaning + * no time information) exists it always at first place as we consider it as "now". * * @return a list of {@link TimestampedLwM2mNode} OR null if this is a error response or "Notification * Storing When Disabled or Offline" is not used. diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java index 4bb533cfd9..114b98117a 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java @@ -153,6 +153,7 @@ public void can_observe_timestamped_resource(ContentFormat contentFormat, String ObserveResponse response = server.waitForNotificationOf(observation); assertThat(response).hasContentFormat(contentFormat, givenServerEndpointProvider); assertThat(response.getContent()).isEqualTo(mostRecentNode.getNode()); + assertThat(response.getTimestampedLwM2mNode()).isEqualTo(mostRecentNode); assertThat(response.getTimestampedLwM2mNodes()).isEqualTo(timestampedNodes); } @@ -178,7 +179,56 @@ public void can_observe_timestamped_instance(ContentFormat contentFormat, String new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Paris"))); List timestampedNodes = new ArrayList<>(); timestampedNodes.add(mostRecentNode); - timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp().minusMillis(2), + timestampedNodes.add(new TimestampedLwM2mNode(Instant.ofEpochMilli(System.currentTimeMillis()).minusMillis(2), + new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Londres")))); + timestampedNodes.add(new TimestampedLwM2mNode(Instant.ofEpochMilli(System.currentTimeMillis()).minusMillis(4), + new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Londres")))); + byte[] payload = encoder.encodeTimestampedData(timestampedNodes, contentFormat, new LwM2mPath("/3/0"), + client.getObjectTree().getModel()); + + TestObserveUtil.sendNotification( + client.getClientConnector(client.getServerIdForRegistrationId(currentRegistration.getId())), + server.getEndpoint(Protocol.COAP).getURI(), payload, + observeResponse.getObservation().getId().getBytes(), 2, contentFormat); + // *** Hack End *** // + + // verify result + server.waitForNewObservation(observation); + ObserveResponse response = server.waitForNotificationOf(observation); + + assertThat(response).hasContentFormat(contentFormat, givenServerEndpointProvider); + assertThat(response.getContent()).isEqualTo(mostRecentNode.getNode()); + assertThat(response.getTimestampedLwM2mNode()).isEqualTo(mostRecentNode); + assertThat(response.getTimestampedLwM2mNodes()).isEqualTo(timestampedNodes); + + } + + @TestAllCases + public void can_observe_timestamped_instance_with_null(ContentFormat contentFormat, + String givenServerEndpointProvider) throws InterruptedException { + // observe device timezone + ObserveResponse observeResponse = server.send(currentRegistration, new ObserveRequest(contentFormat, 3, 0)); + assertThat(observeResponse) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // an observation response should have been sent + SingleObservation observation = observeResponse.getObservation(); + assertThat(observation.getPath()).asString().isEqualTo("/3/0"); + assertThat(observation.getRegistrationId()).isEqualTo(currentRegistration.getId()); + Set observations = server.getObservationService().getObservations(currentRegistration); + assertThat(observations).containsExactly(observation); + + // *** HACK send time-stamped notification as Leshan client does not support it *** // + // create time-stamped nodes + TimestampedLwM2mNode mostRecentNode = new TimestampedLwM2mNode(null, + new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Paris"))); + List timestampedNodes = new ArrayList<>(); + timestampedNodes.add(mostRecentNode); + Instant anInstant = Instant.ofEpochMilli(System.currentTimeMillis()); + timestampedNodes.add(new TimestampedLwM2mNode(anInstant, + new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Londres")))); + timestampedNodes.add(new TimestampedLwM2mNode(anInstant.minusMillis(4), new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Londres")))); byte[] payload = encoder.encodeTimestampedData(timestampedNodes, contentFormat, new LwM2mPath("/3/0"), client.getObjectTree().getModel()); @@ -194,6 +244,7 @@ public void can_observe_timestamped_instance(ContentFormat contentFormat, String ObserveResponse response = server.waitForNotificationOf(observation); assertThat(response).hasContentFormat(contentFormat, givenServerEndpointProvider); assertThat(response.getContent()).isEqualTo(mostRecentNode.getNode()); + assertThat(response.getTimestampedLwM2mNode()).isEqualTo(mostRecentNode); assertThat(response.getTimestampedLwM2mNodes()).isEqualTo(timestampedNodes); } @@ -235,6 +286,7 @@ public void can_observe_timestamped_object(ContentFormat contentFormat, String g ObserveResponse response = server.waitForNotificationOf(observation); assertThat(response).hasContentFormat(contentFormat, givenServerEndpointProvider); assertThat(response.getContent()).isEqualTo(mostRecentNode.getNode()); + assertThat(response.getTimestampedLwM2mNode()).isEqualTo(mostRecentNode); assertThat(response.getTimestampedLwM2mNodes()).isEqualTo(timestampedNodes); } }