diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/lockstep/LockStepTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/lockstep/LockStepTest.java index a10def7d7b..3ebeceafef 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/lockstep/LockStepTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/lockstep/LockStepTest.java @@ -36,8 +36,11 @@ import java.net.URI; import java.time.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; +import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Executors; @@ -59,6 +62,7 @@ import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; +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.Observation; @@ -67,6 +71,7 @@ import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.DeregisterRequest; import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.request.ReadCompositeRequest; import org.eclipse.leshan.core.request.ReadRequest; import org.eclipse.leshan.core.request.RegisterRequest; import org.eclipse.leshan.core.request.UpdateRequest; @@ -75,6 +80,7 @@ import org.eclipse.leshan.core.response.CancelObservationResponse; import org.eclipse.leshan.core.response.ErrorCallback; import org.eclipse.leshan.core.response.ObserveResponse; +import org.eclipse.leshan.core.response.ReadCompositeResponse; import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.core.response.ResponseCallback; import org.eclipse.leshan.integration.tests.util.LeshanTestServer; @@ -524,4 +530,55 @@ public void observe_timestamped(String givenServerEndpointProvider) throws Excep ObserveResponse cancelResponse = cancelFuture.get(1, TimeUnit.SECONDS); assertThat(cancelResponse.getTimestampedLwM2mNode()).isEqualTo(timestampedNode); } + + @TestAllTransportLayer + public void read_composite_timestamped(String givenServerEndpointProvider) throws Exception { + + // register client + LockStepLwM2mClient client = new LockStepLwM2mClient(server.getEndpoint(Protocol.COAP).getURI()); + Token token = client + .sendLwM2mRequest(new RegisterRequest(client.getEndpointName(), 60l, "1.1", EnumSet.of(BindingMode.U), + null, null, linkParser.parseCoreLinkFormat(",,".getBytes()), null)); + client.expectResponse().token(token).go(); + server.waitForNewRegistrationOf(client.getEndpointName()); + + Registration registration = server.getRegistrationService().getByEndpoint(client.getEndpointName()); + + // create timestamped data + + List paths = new ArrayList<>(); + paths.add(new LwM2mPath("/1/0/1")); + paths.add(new LwM2mPath("/3/0/15")); + // and expected Time-stamped nodes + TimestampedLwM2mNodes.Builder builder = new TimestampedLwM2mNodes.Builder(); + Instant t1 = Instant.now().truncatedTo(ChronoUnit.MILLIS); + Instant t2 = Instant.now().truncatedTo(ChronoUnit.MILLIS).minusSeconds(200); + builder.put(t1, paths.get(0), LwM2mSingleResource.newIntegerResource(1, 3600)); + builder.put(t1, paths.get(1), LwM2mSingleResource.newStringResource(15, "Europe/Belgrade")); + TimestampedLwM2mNodes timestampednodes = builder.build(); + + // TEST + LwM2mEncoder encoder = new DefaultLwM2mEncoder(); + + byte[] payload = encoder.encodeTimestampedNodes(timestampednodes, ContentFormat.SENML_JSON, + client.getLwM2mModel()); + + // send read request + Future future = Executors.newSingleThreadExecutor().submit(() -> { + // send a request with 1 seconds timeout + return server.send(registration, + new ReadCompositeRequest(ContentFormat.SENML_JSON, ContentFormat.SENML_JSON, "/1/0/1", "/3/0/15"), + 1000); + }); + + // wait for request and send response + client.expectRequest().storeToken("TKN").storeMID("MID").go(); + client.sendResponse(Type.ACK, ResponseCode.CONTENT).loadMID("MID").loadToken("TKN") + .payload(payload, ContentFormat.SENML_JSON_CODE).go(); + + // check response received at server side + ReadCompositeResponse response = future.get(1, TimeUnit.SECONDS); + assertThat(response.getTimestampedLwM2mNodes()).isEqualTo(timestampednodes); + } + } diff --git a/leshan-lwm2m-client/src/main/java/org/eclipse/leshan/client/request/DefaultDownlinkReceiver.java b/leshan-lwm2m-client/src/main/java/org/eclipse/leshan/client/request/DefaultDownlinkReceiver.java index d3fbcc9277..43d4cf2359 100644 --- a/leshan-lwm2m-client/src/main/java/org/eclipse/leshan/client/request/DefaultDownlinkReceiver.java +++ b/leshan-lwm2m-client/src/main/java/org/eclipse/leshan/client/request/DefaultDownlinkReceiver.java @@ -361,7 +361,7 @@ public void visit(CancelObservationRequest request) { @Override public void visit(ReadCompositeRequest request) { - response = new ReadCompositeResponse(code, null, errorMessage, null); + response = new ReadCompositeResponse(code, null, null, errorMessage, null); } @Override diff --git a/leshan-lwm2m-core/src/main/java/org/eclipse/leshan/core/response/ObserveCompositeResponse.java b/leshan-lwm2m-core/src/main/java/org/eclipse/leshan/core/response/ObserveCompositeResponse.java index efb2887f4a..a9b44573cc 100644 --- a/leshan-lwm2m-core/src/main/java/org/eclipse/leshan/core/response/ObserveCompositeResponse.java +++ b/leshan-lwm2m-core/src/main/java/org/eclipse/leshan/core/response/ObserveCompositeResponse.java @@ -37,7 +37,7 @@ public ObserveCompositeResponse(ResponseCode code, Map con TimestampedLwM2mNodes timestampedValues, CompositeObservation observation, String errorMessage, Object coapResponse) { super(code, timestampedValues != null && !timestampedValues.isEmpty() ? timestampedValues.getNodes() : content, - errorMessage, coapResponse); + null, errorMessage, coapResponse); this.observation = observation; this.timestampedValues = timestampedValues; } diff --git a/leshan-lwm2m-core/src/main/java/org/eclipse/leshan/core/response/ReadCompositeResponse.java b/leshan-lwm2m-core/src/main/java/org/eclipse/leshan/core/response/ReadCompositeResponse.java index e6ec477451..d3326d38ae 100644 --- a/leshan-lwm2m-core/src/main/java/org/eclipse/leshan/core/response/ReadCompositeResponse.java +++ b/leshan-lwm2m-core/src/main/java/org/eclipse/leshan/core/response/ReadCompositeResponse.java @@ -16,19 +16,58 @@ package org.eclipse.leshan.core.response; import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; 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; public class ReadCompositeResponse extends AbstractLwM2mResponse { protected final Map content; - public ReadCompositeResponse(ResponseCode code, Map content, String errorMessage, - Object coapResponse) { + protected final TimestampedLwM2mNodes timestampedValues; + + public ReadCompositeResponse(ResponseCode code, Map content, + TimestampedLwM2mNodes timestampedValues, String errorMessage, Object coapResponse) { super(code, errorMessage, coapResponse); - this.content = content; + + Map responseContent; + TimestampedLwM2mNodes responsetimestampedValues; + + if (timestampedValues != null) { + // handle if timestamped value is passed + if (content != null) { + throw new IllegalArgumentException("content OR timestampedValue should be passed but not both"); + } + // store value if all timestamps in timestampedValues are null + if (!timestampedValues.getNodes().isEmpty() + && timestampedValues.getTimestamps().stream().noneMatch(Objects::nonNull)) { + + responseContent = timestampedValues.getNodes(); + responsetimestampedValues = null; + } else { + // check if we have only timestamp in timestampedValues + if (timestampedValues.getTimestamps().stream() + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())).size() >= 2) { + throw new IllegalArgumentException("only one timestamp in the content is allowed"); + } + + responseContent = null; + responsetimestampedValues = timestampedValues; + } + } else { + // handle if content (not timestamped) value is passed + responsetimestampedValues = null; + responseContent = content; + } + + this.content = responseContent; + this.timestampedValues = responsetimestampedValues; + } public Map getContent() { @@ -39,6 +78,10 @@ public LwM2mNode getContent(String path) { return content.get(new LwM2mPath(path)); } + public TimestampedLwM2mNodes getTimestampedLwM2mNodes() { + return timestampedValues; + } + @Override public boolean isSuccess() { return getCode() == ResponseCode.CONTENT; @@ -71,34 +114,38 @@ public String toString() { // Syntactic sugar static constructors : public static ReadCompositeResponse success(Map content) { - return new ReadCompositeResponse(ResponseCode.CONTENT, content, null, null); + return new ReadCompositeResponse(ResponseCode.CONTENT, content, null, null, null); + } + + public static ReadCompositeResponse success(TimestampedLwM2mNodes timestampedValues) { + return new ReadCompositeResponse(ResponseCode.CONTENT, null, timestampedValues, null, null); } public static ReadCompositeResponse notFound() { - return new ReadCompositeResponse(ResponseCode.NOT_FOUND, null, null, null); + return new ReadCompositeResponse(ResponseCode.NOT_FOUND, null, null, null, null); } public static ReadCompositeResponse unauthorized() { - return new ReadCompositeResponse(ResponseCode.UNAUTHORIZED, null, null, null); + return new ReadCompositeResponse(ResponseCode.UNAUTHORIZED, null, null, null, null); } public static ReadCompositeResponse methodNotAllowed() { - return new ReadCompositeResponse(ResponseCode.METHOD_NOT_ALLOWED, null, null, null); + return new ReadCompositeResponse(ResponseCode.METHOD_NOT_ALLOWED, null, null, null, null); } public static ReadCompositeResponse notAcceptable() { - return new ReadCompositeResponse(ResponseCode.NOT_ACCEPTABLE, null, null, null); + return new ReadCompositeResponse(ResponseCode.NOT_ACCEPTABLE, null, null, null, null); } public static ReadCompositeResponse unsupportedContentFormat() { - return new ReadCompositeResponse(ResponseCode.UNSUPPORTED_CONTENT_FORMAT, null, null, null); + return new ReadCompositeResponse(ResponseCode.UNSUPPORTED_CONTENT_FORMAT, null, null, null, null); } public static ReadCompositeResponse badRequest(String errorMessage) { - return new ReadCompositeResponse(ResponseCode.BAD_REQUEST, null, errorMessage, null); + return new ReadCompositeResponse(ResponseCode.BAD_REQUEST, null, null, errorMessage, null); } public static ReadCompositeResponse internalServerError(String errorMessage) { - return new ReadCompositeResponse(ResponseCode.INTERNAL_SERVER_ERROR, null, errorMessage, null); + return new ReadCompositeResponse(ResponseCode.INTERNAL_SERVER_ERROR, null, null, errorMessage, null); } } diff --git a/leshan-tl-cf-server-coap/src/main/java/org/eclipse/leshan/transport/californium/server/request/LwM2mResponseBuilder.java b/leshan-tl-cf-server-coap/src/main/java/org/eclipse/leshan/transport/californium/server/request/LwM2mResponseBuilder.java index 85d6c833bb..91877ae9c4 100644 --- a/leshan-tl-cf-server-coap/src/main/java/org/eclipse/leshan/transport/californium/server/request/LwM2mResponseBuilder.java +++ b/leshan-tl-cf-server-coap/src/main/java/org/eclipse/leshan/transport/californium/server/request/LwM2mResponseBuilder.java @@ -35,6 +35,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.node.codec.LwM2mDecoder; import org.eclipse.leshan.core.observation.CompositeObservation; @@ -295,13 +296,13 @@ public void visit(CancelObservationRequest request) { public void visit(ReadCompositeRequest request) { if (coapResponse.isError()) { // handle error response: - lwM2mresponse = new ReadCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), null, + lwM2mresponse = new ReadCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), null, null, coapResponse.getPayloadString(), coapResponse); } else if (isResponseCodeContent()) { // handle success response: - Map content = decodeCompositeCoapResponse(request.getPaths(), coapResponse, request, - clientEndpoint); - lwM2mresponse = new ReadCompositeResponse(ResponseCode.CONTENT, content, null, coapResponse); + TimestampedLwM2mNodes timestampedNodes = decodeTimestampedCompositeCoapResponse(request.getPaths(), + coapResponse, request, clientEndpoint); + lwM2mresponse = new ReadCompositeResponse(ResponseCode.CONTENT, null, timestampedNodes, null, coapResponse); } else { // handle unexpected response: handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); @@ -384,6 +385,17 @@ private Map decodeCompositeCoapResponse(List pa } } + private TimestampedLwM2mNodes decodeTimestampedCompositeCoapResponse(List paths, Response coapResponse, + LwM2mRequest request, String endpoint) { + try { + return decoder.decodeTimestampedNodes(coapResponse.getPayload(), getContentFormat(coapResponse), paths, + model); + } catch (CodecException e) { + handleCodecException(e, request, coapResponse, endpoint); + return null; // should not happen as handleCodecException raise exception + } + } + private TimestampedLwM2mNode decodeCoapTimestampedResponse(LwM2mPath path, Response coapResponse, LwM2mRequest request, String endpoint) { List timestampedNodes = null; diff --git a/leshan-tl-jc-server-coap/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/LwM2mResponseBuilder.java b/leshan-tl-jc-server-coap/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/LwM2mResponseBuilder.java index 3e5f253dfc..30cd65e3fa 100644 --- a/leshan-tl-jc-server-coap/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/LwM2mResponseBuilder.java +++ b/leshan-tl-jc-server-coap/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/LwM2mResponseBuilder.java @@ -26,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.CodecException; import org.eclipse.leshan.core.node.codec.LwM2mDecoder; import org.eclipse.leshan.core.observation.CompositeObservation; @@ -295,13 +296,13 @@ public void visit(CancelObservationRequest request) { public void visit(ReadCompositeRequest request) { if (coapResponse.getCode().getHttpCode() >= 400) { // handle error response: - lwM2mresponse = new ReadCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), null, + lwM2mresponse = new ReadCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), null, null, coapResponse.getPayloadString(), coapResponse); } else if (isResponseCodeContent()) { // handle success response: - Map content = decodeCompositeCoapResponse(request.getPaths(), coapResponse, request, - clientEndpoint); - lwM2mresponse = new ReadCompositeResponse(ResponseCode.CONTENT, content, null, coapResponse); + TimestampedLwM2mNodes timestampedNodes = decodeTimestampedCompositeCoapResponse(request.getPaths(), + coapResponse, request, clientEndpoint); + lwM2mresponse = new ReadCompositeResponse(ResponseCode.CONTENT, null, timestampedNodes, null, coapResponse); } else { // handle unexpected response: handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); @@ -499,6 +500,17 @@ private Map decodeCompositeCoapResponse(List pa } } + private TimestampedLwM2mNodes decodeTimestampedCompositeCoapResponse(List paths, + CoapResponse coapResponse, LwM2mRequest request, String endpoint) { + try { + return decoder.decodeTimestampedNodes(coapResponse.getPayload().getBytes(), getContentFormat(coapResponse), + paths, model); + } catch (CodecException e) { + handleCodecException(e, request, coapResponse, endpoint); + return null; // should not happen as handleCodecException raise exception + } + } + private TimestampedLwM2mNode decodeCoapTimestampedResponse(LwM2mPath path, CoapResponse coapResponse, LwM2mRequest request, String endpoint) { List timestampedNodes = null;