From aa5031c329dce91b1a39c2bae92b174683c203c5 Mon Sep 17 00:00:00 2001 From: JaroslawLegierski Date: Mon, 11 Dec 2023 16:56:20 +0100 Subject: [PATCH] Support of ObserveComposite for timestamped data on server side --- .../request/DefaultDownlinkReceiver.java | 2 +- .../core/node/codec/DefaultLwM2mDecoder.java | 6 +- .../leshan/core/node/codec/LwM2mDecoder.java | 4 +- .../codec/TimestampedMultiNodeDecoder.java | 6 +- .../codec/senml/LwM2mNodeSenMLDecoder.java | 3 +- .../CancelCompositeObservationResponse.java | 2 +- .../response/ObserveCompositeResponse.java | 27 +- .../core/node/codec/LwM2mNodeDecoderTest.java | 2 +- .../ObserveCompositeResponseTest.java | 3 +- .../ObserveCompositeTimeStampTest.java | 337 ++++++++++++++++++ .../tests/util/LeshanTestServer.java | 2 +- .../endpoint/ServerCoapMessageTranslator.java | 20 +- .../request/LwM2mResponseBuilder.java | 4 +- .../server/californium/send/SendResource.java | 2 +- .../server/californium/DummyDecoder.java | 4 +- .../observation/CoapNotificationReceiver.java | 20 +- .../server/request/LwM2mResponseBuilder.java | 6 +- .../server/resource/SendResource.java | 2 +- 18 files changed, 416 insertions(+), 36 deletions(-) create mode 100644 leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTimeStampTest.java diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/request/DefaultDownlinkReceiver.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/request/DefaultDownlinkReceiver.java index 48cc0f3404..d2facf0953 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/request/DefaultDownlinkReceiver.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/request/DefaultDownlinkReceiver.java @@ -367,7 +367,7 @@ public void visit(ReadCompositeRequest request) { @Override public void visit(ObserveCompositeRequest request) { - response = new ObserveCompositeResponse(code, null, errorMessage, null, null); + response = new ObserveCompositeResponse(code, null, errorMessage, null, null, null); } @Override diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mDecoder.java index f961b36a7b..fe85a84d03 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mDecoder.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mDecoder.java @@ -207,8 +207,8 @@ public List decodeTimestampedData(byte[] content, ContentF } @Override - public TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, ContentFormat format, LwM2mModel model) - throws CodecException { + public TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, ContentFormat format, LwM2mModel model, + List paths) throws CodecException { LOG.trace("Decoding value for format {}: {}", format, content); if (format == null) { @@ -221,7 +221,7 @@ public TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, ContentForma } if (decoder instanceof TimestampedMultiNodeDecoder) { - return ((TimestampedMultiNodeDecoder) decoder).decodeTimestampedNodes(content, model); + return ((TimestampedMultiNodeDecoder) decoder).decodeTimestampedNodes(content, model, paths); } else if (decoder instanceof MultiNodeDecoder) { return new TimestampedLwM2mNodes.Builder() .addNodes(((MultiNodeDecoder) decoder).decodeNodes(content, null, model)).build(); diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mDecoder.java index 540a56ab75..50fff17948 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mDecoder.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mDecoder.java @@ -108,8 +108,8 @@ List decodeTimestampedData(byte[] content, ContentFormat f * @return the decoded timestamped nodes represented by {@link TimestampedLwM2mNodes} * @throws CodecException if content is malformed. */ - TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, ContentFormat format, LwM2mModel model) - throws CodecException; + TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, ContentFormat format, LwM2mModel model, + List paths) throws CodecException; /** * Deserializes a binary content into a list of {@link LwM2mPath}. diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedMultiNodeDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedMultiNodeDecoder.java index dba265c54b..d35b5b2d18 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedMultiNodeDecoder.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedMultiNodeDecoder.java @@ -15,7 +15,10 @@ *******************************************************************************/ package org.eclipse.leshan.core.node.codec; +import java.util.List; + import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; /** @@ -33,6 +36,7 @@ public interface TimestampedMultiNodeDecoder { * @return the decoded timestamped nodes represented by {@link TimestampedLwM2mNodes} * @throws CodecException if content is malformed. */ - TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, LwM2mModel model) throws CodecException; + TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, LwM2mModel model, List paths) + throws CodecException; } 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 3b325c3076..4422f6d364 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 @@ -193,7 +193,8 @@ public List decodeTimestampedData(byte[] content, LwM2mPat } @Override - public TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, LwM2mModel model) throws CodecException { + public TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, LwM2mModel model, List paths) + throws CodecException { try { // Decode SenML pack SenMLPack pack = decoder.fromSenML(content); diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/response/CancelCompositeObservationResponse.java b/leshan-core/src/main/java/org/eclipse/leshan/core/response/CancelCompositeObservationResponse.java index eaed18c569..cb24dd8799 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/response/CancelCompositeObservationResponse.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/response/CancelCompositeObservationResponse.java @@ -26,7 +26,7 @@ public class CancelCompositeObservationResponse extends ObserveCompositeResponse public CancelCompositeObservationResponse(ResponseCode code, Map content, String errorMessage, Object coapResponse, CompositeObservation observation) { - super(code, content, errorMessage, coapResponse, observation); + super(code, content, errorMessage, coapResponse, observation, null); } @Override diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveCompositeResponse.java b/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveCompositeResponse.java index 586c7d1904..3a8ae2c013 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveCompositeResponse.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveCompositeResponse.java @@ -20,6 +20,7 @@ import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.observation.CompositeObservation; /** @@ -30,11 +31,13 @@ public class ObserveCompositeResponse extends ReadCompositeResponse { protected final CompositeObservation observation; + protected final TimestampedLwM2mNodes timestampedValues; public ObserveCompositeResponse(ResponseCode code, Map content, String errorMessage, - Object coapResponse, CompositeObservation observation) { + Object coapResponse, CompositeObservation observation, TimestampedLwM2mNodes timestampedValues) { super(code, content, errorMessage, coapResponse); this.observation = observation; + this.timestampedValues = timestampedValues; } public CompositeObservation getObservation() { @@ -69,30 +72,38 @@ public boolean isValid() { // Syntactic sugar static constructors: public static ObserveCompositeResponse success(Map content) { - return new ObserveCompositeResponse(ResponseCode.CONTENT, content, null, null, null); + return new ObserveCompositeResponse(ResponseCode.CONTENT, content, null, null, null, null); + } + + public static ObserveCompositeResponse success(TimestampedLwM2mNodes timestampedValues) { + return new ObserveCompositeResponse(ResponseCode.CONTENT, null, null, null, null, timestampedValues); } public static ObserveCompositeResponse badRequest(String errorMessage) { - return new ObserveCompositeResponse(ResponseCode.BAD_REQUEST, null, errorMessage, null, null); + return new ObserveCompositeResponse(ResponseCode.BAD_REQUEST, null, errorMessage, null, null, null); } public static ObserveCompositeResponse notFound() { - return new ObserveCompositeResponse(ResponseCode.NOT_FOUND, null, null, null, null); + return new ObserveCompositeResponse(ResponseCode.NOT_FOUND, null, null, null, null, null); } public static ObserveCompositeResponse unauthorized() { - return new ObserveCompositeResponse(ResponseCode.UNAUTHORIZED, null, null, null, null); + return new ObserveCompositeResponse(ResponseCode.UNAUTHORIZED, null, null, null, null, null); } public static ObserveCompositeResponse methodNotAllowed() { - return new ObserveCompositeResponse(ResponseCode.METHOD_NOT_ALLOWED, null, null, null, null); + return new ObserveCompositeResponse(ResponseCode.METHOD_NOT_ALLOWED, null, null, null, null, null); } public static ObserveCompositeResponse notAcceptable() { - return new ObserveCompositeResponse(ResponseCode.UNSUPPORTED_CONTENT_FORMAT, null, null, null, null); + return new ObserveCompositeResponse(ResponseCode.UNSUPPORTED_CONTENT_FORMAT, null, null, null, null, null); } public static ObserveCompositeResponse internalServerError(String errorMessage) { - return new ObserveCompositeResponse(ResponseCode.INTERNAL_SERVER_ERROR, null, errorMessage, null, null); + return new ObserveCompositeResponse(ResponseCode.INTERNAL_SERVER_ERROR, null, errorMessage, null, null, null); + } + + public TimestampedLwM2mNodes getTimestampedLwM2mNodes() { + return timestampedValues; } } 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 73da07980c..ca958afdef 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 @@ -1354,7 +1354,7 @@ public void senml_multiple_timestamped_nodes() throws CodecException { // when TimestampedLwM2mNodes data = decoder.decodeTimestampedNodes(b.toString().getBytes(), ContentFormat.SENML_JSON, - model); + model, null); // then Instant timestamp = Instant.ofEpochSecond(268600000); diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/response/ObserveCompositeResponseTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/response/ObserveCompositeResponseTest.java index cc7065b1f3..c0457239d7 100644 --- a/leshan-core/src/test/java/org/eclipse/leshan/core/response/ObserveCompositeResponseTest.java +++ b/leshan-core/src/test/java/org/eclipse/leshan/core/response/ObserveCompositeResponseTest.java @@ -36,7 +36,8 @@ public void should_create_response_with_content() { exampleContent.put(new LwM2mPath("/2/3/4"), newResource(16, "example 2")); // when - ObserveCompositeResponse response = new ObserveCompositeResponse(CONTENT, exampleContent, null, null, null); + ObserveCompositeResponse response = new ObserveCompositeResponse(CONTENT, exampleContent, null, null, null, + null); // then assertEquals(exampleContent, response.getContent()); diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTimeStampTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTimeStampTest.java new file mode 100644 index 0000000000..8db5080f15 --- /dev/null +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTimeStampTest.java @@ -0,0 +1,337 @@ +/******************************************************************************* + * Copyright (c) 2020 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Jarosław Legierski Orange Polska S.A. - initial API and implementation + *******************************************************************************/ + +package org.eclipse.leshan.integration.tests.observe; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.leshan.core.ResponseCode.CONTENT; +import static org.eclipse.leshan.integration.tests.util.LeshanTestClientBuilder.givenClientUsing; +import static org.eclipse.leshan.integration.tests.util.LeshanTestServerBuilder.givenServerUsing; +import static org.eclipse.leshan.integration.tests.util.assertion.Assertions.assertThat; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import org.eclipse.leshan.core.endpoint.Protocol; +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.TimestampedLwM2mNodes; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; +import org.eclipse.leshan.core.node.codec.LwM2mEncoder; +import org.eclipse.leshan.core.observation.CompositeObservation; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; +import org.eclipse.leshan.integration.tests.util.LeshanTestClient; +import org.eclipse.leshan.integration.tests.util.LeshanTestServer; +import org.eclipse.leshan.integration.tests.util.junit5.extensions.ArgumentsUtil; +import org.eclipse.leshan.integration.tests.util.junit5.extensions.BeforeEachParameterizedResolver; +import org.eclipse.leshan.server.registration.Registration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(BeforeEachParameterizedResolver.class) +public class ObserveCompositeTimeStampTest { + + /*---------------------------------/ + * Parameterized Tests + * -------------------------------*/ + @ParameterizedTest(name = "{0} over COAP - Client using Californium - Server using {1}") + @MethodSource("transports") + @Retention(RetentionPolicy.RUNTIME) + private @interface TestAllCases { + } + + static Stream transports() { + + Object[][] transports = new Object[][] { + // Server Endpoint Provider + { "Californium" }, // + { "java-coap" } }; +// { "Californium" } }; + + Object[] contentFormats = new Object[] { // + ContentFormat.SENML_JSON, // + ContentFormat.SENML_CBOR }; +// ContentFormat.SENML_JSON }; + + // for each transport, create 1 test by format. + return Stream.of(ArgumentsUtil.combine(contentFormats, transports)); + } + + /*---------------------------------/ + * Set-up and Tear-down Tests + * -------------------------------*/ + + LeshanTestServer server; + LeshanTestClient client; + Registration currentRegistration; + LwM2mEncoder encoder = new DefaultLwM2mEncoder(); + + @BeforeEach + public void start(ContentFormat format, String givenServerEndpointProvider) { + server = givenServerUsing(Protocol.COAP).with(givenServerEndpointProvider).build(); + server.start(); + client = givenClientUsing(Protocol.COAP).with("Californium").connectingTo(server).build(); + client.start(); + server.waitForNewRegistrationOf(client); + client.waitForRegistrationTo(server); + + currentRegistration = server.getRegistrationFor(client); + + } + + @AfterEach + public void stop() throws InterruptedException { + if (client != null) + client.destroy(false); + if (server != null) + server.destroy(); + } + + /*---------------------------------/ + * Tests + * -------------------------------*/ + @TestAllCases + public void can_observecomposite_timestamped_resource(ContentFormat contentFormat, + String givenServerEndpointProvider) throws InterruptedException { + // observe device timezone + + // LwM2mPaths + List paths = new ArrayList<>(); + paths.add(new LwM2mPath("/1/0/1")); + paths.add(new LwM2mPath("/3/0/15")); + + ObserveCompositeResponse observeResponse = server.send(currentRegistration, + new ObserveCompositeRequest(contentFormat, contentFormat, paths)); + assertThat(observeResponse) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // an observation response should have been sent + CompositeObservation observation = observeResponse.getObservation(); + + assertThat(observation.getPaths()).asString().isEqualTo("[/1/0/1, /3/0/15]"); + 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 + + TimestampedLwM2mNodes.Builder builder = new TimestampedLwM2mNodes.Builder(); + Map currentValues = new HashMap<>(); + currentValues.put(paths.get(0), LwM2mSingleResource.newIntegerResource(1, 3600)); + builder.addNodes(Instant.ofEpochMilli(System.currentTimeMillis()), currentValues); + currentValues.clear(); + currentValues.put(paths.get(1), LwM2mSingleResource.newStringResource(15, "Europe/Belgrade")); + builder.addNodes(Instant.ofEpochMilli(System.currentTimeMillis()), currentValues); + TimestampedLwM2mNodes timestampednodes = builder.build(); + + byte[] payload = encoder.encodeTimestampedNodes(timestampednodes, contentFormat, + client.getObjectTree().getModel()); + + TestObserveUtil.sendNotification( + client.getClientConnector(client.getServerIdForRegistrationId("/rd/" + currentRegistration.getId())), + server.getEndpoint(Protocol.COAP).getURI(), payload, + observeResponse.getObservation().getId().getBytes(), 2, contentFormat); + // *** Hack End *** // + + // verify result + server.waitForNewObservation(observation); + ObserveCompositeResponse response = server.waitForNotificationOf(observation); + assertThat(response).hasContentFormat(contentFormat, givenServerEndpointProvider); + assertThat(response.getContent().get(new LwM2mPath("/1/0/1"))) + .isEqualTo(timestampednodes.getNodes().get(new LwM2mPath("/1/0/1"))); + assertThat(response.getContent().get(new LwM2mPath("/3/0/15"))) + .isEqualTo(timestampednodes.getNodes().get(new LwM2mPath("/3/0/15"))); + assertThat(response.getTimestampedLwM2mNodes()).isEqualTo(timestampednodes); + } + + @TestAllCases + public void can_observecomposite_timestamped_instance(ContentFormat contentFormat, + String givenServerEndpointProvider) throws InterruptedException { + + // LwM2mPaths + List paths = new ArrayList<>(); + paths.add(new LwM2mPath("/1/0/1")); + paths.add(new LwM2mPath("/3/0")); + + // observe device timezone + ObserveCompositeResponse observeResponse = server.send(currentRegistration, + new ObserveCompositeRequest(contentFormat, contentFormat, "/1/0/1", "/3/0")); + assertThat(observeResponse) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // an observation response should have been sent + CompositeObservation observation = observeResponse.getObservation(); + assertThat(observation.getPaths()).asString().isEqualTo("[/1/0/1, /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 + + List deviceresources = new ArrayList(); + deviceresources.add(LwM2mSingleResource.newStringResource(0, "Leshan Demo Device")); + deviceresources.add(LwM2mSingleResource.newStringResource(1, "Model 500")); + deviceresources.add(LwM2mSingleResource.newStringResource(2, "LT-500-000-0001")); + deviceresources.add(LwM2mSingleResource.newStringResource(3, "1.0.0")); + deviceresources.add(LwM2mSingleResource.newIntegerResource(9, 75)); + deviceresources.add(LwM2mSingleResource.newIntegerResource(10, 423455)); + deviceresources.add(LwM2mSingleResource.newIntegerResource(11, 0)); + deviceresources.add(LwM2mSingleResource.newDateResource(13, new Date(1702050067000L))); + deviceresources.add(LwM2mSingleResource.newStringResource(14, "+01")); + deviceresources.add(LwM2mSingleResource.newStringResource(15, "Europe/Belgrade")); + deviceresources.add(LwM2mSingleResource.newStringResource(16, "U")); + deviceresources.add(LwM2mSingleResource.newStringResource(17, "Demo")); + deviceresources.add(LwM2mSingleResource.newStringResource(18, "1.0.1")); + deviceresources.add(LwM2mSingleResource.newStringResource(19, "1.0.2")); + deviceresources.add(LwM2mSingleResource.newIntegerResource(20, 4)); + deviceresources.add(LwM2mSingleResource.newIntegerResource(21, 20500736)); + + TimestampedLwM2mNodes.Builder builder = new TimestampedLwM2mNodes.Builder(); + Map currentValues = new HashMap<>(); + currentValues.put(paths.get(0), LwM2mSingleResource.newIntegerResource(1, 3600)); + currentValues.put(paths.get(1), new LwM2mObjectInstance(0, deviceresources)); + builder.addNodes(Instant.ofEpochMilli(System.currentTimeMillis()), currentValues); + TimestampedLwM2mNodes timestampednodes = builder.build(); + + byte[] payload = encoder.encodeTimestampedNodes(timestampednodes, contentFormat, + client.getObjectTree().getModel()); + + TestObserveUtil.sendNotification( + client.getClientConnector(client.getServerIdForRegistrationId("/rd/" + currentRegistration.getId())), + server.getEndpoint(Protocol.COAP).getURI(), payload, + observeResponse.getObservation().getId().getBytes(), 2, contentFormat); + // *** Hack End *** // + + // verify result + server.waitForNewObservation(observation); + ObserveCompositeResponse response = server.waitForNotificationOf(observation); + + assertThat(response).hasContentFormat(contentFormat, givenServerEndpointProvider); + + assertThat(response.getTimestampedLwM2mNodes().getNodes().get(new LwM2mPath("/1/0/1"))) + .isEqualTo((timestampednodes.getNodes().get(new LwM2mPath("/1/0/1")))); + + for (LwM2mResource deviceresource : deviceresources) { + assertThat( + response.getTimestampedLwM2mNodes().getNodes().get(new LwM2mPath("/3/0/" + deviceresource.getId()))) + .isEqualTo(((LwM2mObjectInstance) timestampednodes.getNodes().get(new LwM2mPath("/3/0"))) + .getResource(deviceresource.getId())); + } + assertThat(response.getTimestampedLwM2mNodes().getTimestamps()).isEqualTo(timestampednodes.getTimestamps()); + + } + + @TestAllCases + public void can_observecomposite__timestamped_object(ContentFormat contentFormat, + String givenServerEndpointProvider) throws InterruptedException { + // LwM2mPaths + List paths = new ArrayList<>(); + paths.add(new LwM2mPath("/1/0/1")); + paths.add(new LwM2mPath("/3")); + + // observe device timezone + ObserveCompositeResponse observeResponse = server.send(currentRegistration, + new ObserveCompositeRequest(contentFormat, contentFormat, "/1/0/1", "/3")); + assertThat(observeResponse) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // an observation response should have been sent + CompositeObservation observation = observeResponse.getObservation(); + assertThat(observation.getPaths()).asString().isEqualTo("[/1/0/1, /3]"); + 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 + + List deviceresources = new ArrayList(); + deviceresources.add(LwM2mSingleResource.newStringResource(0, "Leshan Demo Device")); + deviceresources.add(LwM2mSingleResource.newStringResource(1, "Model 500")); + deviceresources.add(LwM2mSingleResource.newStringResource(2, "LT-500-000-0001")); + deviceresources.add(LwM2mSingleResource.newStringResource(3, "1.0.0")); + deviceresources.add(LwM2mSingleResource.newIntegerResource(9, 75)); + deviceresources.add(LwM2mSingleResource.newIntegerResource(10, 423455)); + deviceresources.add(LwM2mSingleResource.newIntegerResource(11, 0)); + deviceresources.add(LwM2mSingleResource.newDateResource(13, new Date(1702050067000L))); + deviceresources.add(LwM2mSingleResource.newStringResource(14, "+01")); + deviceresources.add(LwM2mSingleResource.newStringResource(15, "Europe/Belgrade")); + deviceresources.add(LwM2mSingleResource.newStringResource(16, "U")); + deviceresources.add(LwM2mSingleResource.newStringResource(17, "Demo")); + deviceresources.add(LwM2mSingleResource.newStringResource(18, "1.0.1")); + deviceresources.add(LwM2mSingleResource.newStringResource(19, "1.0.2")); + deviceresources.add(LwM2mSingleResource.newIntegerResource(20, 4)); + deviceresources.add(LwM2mSingleResource.newIntegerResource(21, 20500736)); + + TimestampedLwM2mNodes.Builder builder = new TimestampedLwM2mNodes.Builder(); + Map currentValues = new HashMap<>(); + currentValues.put(paths.get(0), LwM2mSingleResource.newIntegerResource(1, 3600)); + currentValues.put(paths.get(1), new LwM2mObject(3, new LwM2mObjectInstance(0, deviceresources))); + builder.addNodes(Instant.ofEpochMilli(System.currentTimeMillis()), currentValues); + TimestampedLwM2mNodes timestampednodes = builder.build(); + + byte[] payload = encoder.encodeTimestampedNodes(timestampednodes, contentFormat, + client.getObjectTree().getModel()); + + TestObserveUtil.sendNotification( + client.getClientConnector(client.getServerIdForRegistrationId("/rd/" + currentRegistration.getId())), + server.getEndpoint(Protocol.COAP).getURI(), payload, + observeResponse.getObservation().getId().getBytes(), 2, contentFormat); + // *** Hack End *** // + + // verify result + server.waitForNewObservation(observation); + ObserveCompositeResponse response = server.waitForNotificationOf(observation); + + assertThat(response).hasContentFormat(contentFormat, givenServerEndpointProvider); + + assertThat(response.getTimestampedLwM2mNodes().getNodes().get(new LwM2mPath("/1/0/1"))) + .isEqualTo((timestampednodes.getNodes().get(new LwM2mPath("/1/0/1")))); + + for (LwM2mResource deviceresource : deviceresources) { + assertThat( + response.getTimestampedLwM2mNodes().getNodes().get(new LwM2mPath("/3/0/" + deviceresource.getId()))) + .isEqualTo(((LwM2mObject) timestampednodes.getNodes().get(new LwM2mPath("/3"))) + .getInstances().get(0).getResource(deviceresource.getId())); + } + assertThat(response.getTimestampedLwM2mNodes().getTimestamps()).isEqualTo(timestampednodes.getTimestamps()); + } + +} diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServer.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServer.java index 891f2728a2..5d32a46947 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServer.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServer.java @@ -146,7 +146,7 @@ public Registration getRegistrationFor(LeshanTestClient client) { } public void waitForNewRegistrationOf(LeshanTestClient client) { - waitForNewRegistrationOf(client, 1, TimeUnit.SECONDS); + waitForNewRegistrationOf(client, 2, TimeUnit.SECONDS); } public void waitForNewRegistrationOf(String clientEndpoint) { diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/endpoint/ServerCoapMessageTranslator.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/endpoint/ServerCoapMessageTranslator.java index c96e539689..8796a27563 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/endpoint/ServerCoapMessageTranslator.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/endpoint/ServerCoapMessageTranslator.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; @@ -31,6 +32,7 @@ import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; @@ -122,10 +124,22 @@ public AbstractLwM2mResponse createObservation(Observation observation, Response CompositeObservation compositeObservation = (CompositeObservation) observation; - Map nodes = toolbox.getDecoder().decodeNodes(coapResponse.getPayload(), - contentFormat, compositeObservation.getPaths(), profile.getModel()); + TimestampedLwM2mNodes timestampedNodes = toolbox.getDecoder().decodeTimestampedNodes( + coapResponse.getPayload(), contentFormat, profile.getModel(), compositeObservation.getPaths()); - return new ObserveCompositeResponse(responseCode, nodes, null, coapResponse, compositeObservation); + if (timestampedNodes != null && !timestampedNodes.isEmpty() + && !timestampedNodes.getTimestamps().stream().noneMatch(Objects::nonNull)) { + + return new ObserveCompositeResponse(responseCode, timestampedNodes.getNodes(), null, coapResponse, + compositeObservation, timestampedNodes); + } else { + + Map nodes = toolbox.getDecoder().decodeNodes(coapResponse.getPayload(), + contentFormat, compositeObservation.getPaths(), profile.getModel()); + + return new ObserveCompositeResponse(responseCode, nodes, null, coapResponse, compositeObservation, + null); + } } throw new IllegalStateException( diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java index 8e247fac6e..f202d78b88 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java @@ -318,7 +318,7 @@ public void visit(ObserveCompositeRequest request) { if (coapResponse.isError()) { // handle error response: lwM2mresponse = new ObserveCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), null, - coapResponse.getPayloadString(), coapResponse, null); + coapResponse.getPayloadString(), coapResponse, null, null); } else if (isResponseCodeContent()) { // handle success response: @@ -331,7 +331,7 @@ public void visit(ObserveCompositeRequest request) { observation = ObserveUtil.createLwM2mCompositeObservation(coapRequest); } lwM2mresponse = new ObserveCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null, - coapResponse, observation); + coapResponse, observation, null); } else { // handle unexpected response: handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/send/SendResource.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/send/SendResource.java index cde6de88e9..399e388304 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/send/SendResource.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/send/SendResource.java @@ -79,7 +79,7 @@ public void handlePOST(CoapExchange exchange) { } TimestampedLwM2mNodes data = decoder.decodeTimestampedNodes(payload, contentFormat, - clientProfile.getModel()); + clientProfile.getModel(), null); // Handle "send op request SendRequest sendRequest = new SendRequest(contentFormat, data, coapRequest); diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/DummyDecoder.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/DummyDecoder.java index ade611be2e..1603c78917 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/DummyDecoder.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/DummyDecoder.java @@ -56,8 +56,8 @@ public List decodeTimestampedData(byte[] content, ContentF } @Override - public TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, ContentFormat format, LwM2mModel model) - throws CodecException { + public TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, ContentFormat format, LwM2mModel model, + List paths) throws CodecException { return null; } diff --git a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/CoapNotificationReceiver.java b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/CoapNotificationReceiver.java index 9fef24a4a6..2185d3c13c 100644 --- a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/CoapNotificationReceiver.java +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/CoapNotificationReceiver.java @@ -18,6 +18,7 @@ import java.net.InetSocketAddress; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -25,6 +26,7 @@ import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.node.codec.LwM2mDecoder; import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; @@ -160,10 +162,20 @@ public AbstractLwM2mResponse createLwM2mResponseForNotification(Observation obse CompositeObservation compositeObservation = (CompositeObservation) observation; ContentFormat contentFormat = ContentFormat.fromCode(coapResponse.options().getContentFormat()); - Map nodes = decoder.decodeNodes(coapResponse.getPayload().getBytes(), contentFormat, - compositeObservation.getPaths(), profile.getModel()); - - return new ObserveCompositeResponse(responseCode, nodes, null, coapResponse, compositeObservation); + TimestampedLwM2mNodes timestampedNodes = decoder.decodeTimestampedNodes( + coapResponse.getPayload().getBytes(), contentFormat, profile.getModel(), + compositeObservation.getPaths()); + if (timestampedNodes != null && !timestampedNodes.isEmpty() + && !timestampedNodes.getTimestamps().stream().noneMatch(Objects::nonNull)) { + + return new ObserveCompositeResponse(responseCode, timestampedNodes.getNodes(), null, coapResponse, + compositeObservation, timestampedNodes); + } else { + Map nodes = decoder.decodeNodes(coapResponse.getPayload().getBytes(), + contentFormat, compositeObservation.getPaths(), profile.getModel()); + return new ObserveCompositeResponse(responseCode, nodes, null, coapResponse, compositeObservation, + null); + } } return null; } diff --git a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/LwM2mResponseBuilder.java b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/LwM2mResponseBuilder.java index 50fbd50510..a080ede948 100644 --- a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/LwM2mResponseBuilder.java +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/LwM2mResponseBuilder.java @@ -319,7 +319,7 @@ public void visit(ObserveCompositeRequest request) { if (coapResponse.getCode().getHttpCode() >= 400) { // handle error response: lwM2mresponse = new ObserveCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), null, - coapResponse.getPayloadString(), coapResponse, null); + coapResponse.getPayloadString(), coapResponse, null, null); } else if (isResponseCodeContent()) { // handle success response: @@ -331,7 +331,7 @@ public void visit(ObserveCompositeRequest request) { Observation observation = coapRequest.getTransContext().get(LwM2mKeys.LESHAN_OBSERVATION); if (observation instanceof CompositeObservation) { lwM2mresponse = new ObserveCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), content, - null, coapResponse, (CompositeObservation) observation); + null, coapResponse, (CompositeObservation) observation, null); } else { throw new IllegalStateException(String.format( "A Composite Observation is expected in coapRequest transport Context, but was %s", @@ -340,7 +340,7 @@ public void visit(ObserveCompositeRequest request) { } else { // Observe relation NOTestablished lwM2mresponse = new ObserveCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null, - coapResponse, null); + coapResponse, null, null); } } else { // handle unexpected response: diff --git a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/resource/SendResource.java b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/resource/SendResource.java index f4f83f62f7..448fce3c62 100644 --- a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/resource/SendResource.java +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/resource/SendResource.java @@ -89,7 +89,7 @@ public CompletableFuture handlePOST(CoapRequest coapRequest) { } TimestampedLwM2mNodes data = decoder.decodeTimestampedNodes(payload, contentFormat, - clientProfile.getModel()); + clientProfile.getModel(), null); // Handle "send op request SendRequest sendRequest = new SendRequest(contentFormat, data, coapRequest);