From 624ad251bc9647d9ddecf97ddf4c3cbefebf606c Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Thu, 13 Apr 2023 17:08:35 +0200 Subject: [PATCH 1/3] Add CoAP endpoint provider based on java-coap at Server side --- build-config/lib-build-config/pom.xml | 7 + .../logback-leshan-test.xml | 35 ++ leshan-tl-javacoap-core/pom.xml | 42 ++ .../leshan/transport/javacoap/State.java | 33 ++ .../request/RandomTokenGenerator.java | 38 ++ .../javacoap/request/ResponseCodeUtil.java | 56 ++ .../javacoap/resource/LwM2mCoapResource.java | 208 +++++++ .../logback-leshan-test.xml | 35 ++ leshan-tl-javacoap-server/pom.xml | 42 ++ .../server/endpoint/EndpointUriProvider.java | 41 ++ .../endpoint/JavaCoapServerEndpoint.java | 243 ++++++++ .../JavaCoapServerEndpointsProvider.java | 138 +++++ .../endpoint/ServerCoapMessageTranslator.java | 49 ++ .../observation/CoapNotificationReceiver.java | 171 ++++++ .../server/observation/LwM2mKeys.java | 28 + .../observation/LwM2mObservationsStore.java | 115 ++++ .../server/observation/ObservationUtil.java | 39 ++ .../server/request/CoapRequestBuilder.java | 298 ++++++++++ .../server/request/LwM2mResponseBuilder.java | 542 ++++++++++++++++++ .../server/resource/RegistrationResource.java | 254 ++++++++ .../server/resource/SendResource.java | 120 ++++ pom.xml | 50 +- 22 files changed, 2571 insertions(+), 13 deletions(-) create mode 100644 leshan-tl-javacoap-core/logback-leshan-test.xml create mode 100644 leshan-tl-javacoap-core/pom.xml create mode 100644 leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/State.java create mode 100644 leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/request/RandomTokenGenerator.java create mode 100644 leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/request/ResponseCodeUtil.java create mode 100644 leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/resource/LwM2mCoapResource.java create mode 100644 leshan-tl-javacoap-server/logback-leshan-test.xml create mode 100644 leshan-tl-javacoap-server/pom.xml create mode 100644 leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/EndpointUriProvider.java create mode 100644 leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/JavaCoapServerEndpoint.java create mode 100644 leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/JavaCoapServerEndpointsProvider.java create mode 100644 leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/ServerCoapMessageTranslator.java create mode 100644 leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/CoapNotificationReceiver.java create mode 100644 leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/LwM2mKeys.java create mode 100644 leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/LwM2mObservationsStore.java create mode 100644 leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/ObservationUtil.java create mode 100644 leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/CoapRequestBuilder.java create mode 100644 leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/LwM2mResponseBuilder.java create mode 100644 leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/resource/RegistrationResource.java create mode 100644 leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/resource/SendResource.java diff --git a/build-config/lib-build-config/pom.xml b/build-config/lib-build-config/pom.xml index 0b750dcb43..b1a124e275 100644 --- a/build-config/lib-build-config/pom.xml +++ b/build-config/lib-build-config/pom.xml @@ -97,6 +97,13 @@ Contributors: java-package /org\.slf4j(\..*)?/ + + + java-package + /com\.mbed\.coap(\..*)?/ + diff --git a/leshan-tl-javacoap-core/logback-leshan-test.xml b/leshan-tl-javacoap-core/logback-leshan-test.xml new file mode 100644 index 0000000000..2e93285e57 --- /dev/null +++ b/leshan-tl-javacoap-core/logback-leshan-test.xml @@ -0,0 +1,35 @@ + + + + + + + %d %p %C{1} [%t] %m%n + + + + + + + + diff --git a/leshan-tl-javacoap-core/pom.xml b/leshan-tl-javacoap-core/pom.xml new file mode 100644 index 0000000000..ba6261941d --- /dev/null +++ b/leshan-tl-javacoap-core/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + org.eclipse.leshan + lib-build-config + 2.0.0-SNAPSHOT + ../build-config/lib-build-config/pom.xml + + leshan-tl-javacoap-core + bundle + leshan - core java-coap + Shared classes between server and client which depends on Java CoAP + + + + org.eclipse.leshan + leshan-core + + + io.github.open-coap + coap-core + + + diff --git a/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/State.java b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/State.java new file mode 100644 index 0000000000..45f6dda729 --- /dev/null +++ b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/State.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap; + +public enum State { + INITIAL, STARTED, STOPPED, DESTROYED; + + public boolean isStarted() { + return this.equals(STARTED); + } + + public boolean isStopped() { + return this.equals(STOPPED); + } + + public boolean isDestroyed() { + return this.equals(DESTROYED); + } + +} diff --git a/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/request/RandomTokenGenerator.java b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/request/RandomTokenGenerator.java new file mode 100644 index 0000000000..908acfafac --- /dev/null +++ b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/request/RandomTokenGenerator.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.request; + +import java.security.SecureRandom; + +import com.mbed.coap.packet.Opaque; + +public class RandomTokenGenerator { + + private final int tokenSize; + private final SecureRandom random; + + public RandomTokenGenerator(int tokenSize) { + // TODO check size is between 1 and 8; + random = new SecureRandom(); + this.tokenSize = tokenSize; + } + + public Opaque createToken() { + byte[] token = new byte[tokenSize]; + random.nextBytes(token); + return Opaque.of(token); + } +} diff --git a/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/request/ResponseCodeUtil.java b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/request/ResponseCodeUtil.java new file mode 100644 index 0000000000..d38fd7fb1e --- /dev/null +++ b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/request/ResponseCodeUtil.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.request; + +import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.util.Validate; + +import com.mbed.coap.packet.Code; + +public class ResponseCodeUtil { + + public static ResponseCode toLwM2mResponseCode(Code coapResponseCode) { + return ResponseCode.fromCode(toLwM2mCode(coapResponseCode.getCoapCode())); + } + + public static int toLwM2mCode(int coapCode) { + int codeClass = (coapCode & 0b11100000) >> 5; + int codeDetail = coapCode & 0b00011111; + return codeClass * 100 + codeDetail; + } + + public static ResponseCode toLwM2mResponseCode(int coapCode) { + return ResponseCode.fromCode(toLwM2mCode(coapCode)); + } + + public static int toCoapCode(int lwm2mCode) { + int codeClass = lwm2mCode / 100; + int codeDetail = lwm2mCode % 100; + if (codeClass > 7 || codeDetail > 31) + throw new IllegalArgumentException("Could not be translated into a valid COAP code"); + + return codeClass << 5 | codeDetail; + } + + public static Code toCoapResponseCode(ResponseCode Lwm2mResponseCode) { + Validate.notNull(Lwm2mResponseCode); + Code result = Code.valueOf(toCoapCode(Lwm2mResponseCode.getCode())); + if (result == null) { + throw new IllegalArgumentException("Unknown CoAP code for LWM2M response: " + Lwm2mResponseCode); + } + return result; + } +} diff --git a/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/resource/LwM2mCoapResource.java b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/resource/LwM2mCoapResource.java new file mode 100644 index 0000000000..2d2eacf2a0 --- /dev/null +++ b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/resource/LwM2mCoapResource.java @@ -0,0 +1,208 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.resource; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.peer.IpPeer; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.exception.InvalidRequestException; +import org.eclipse.leshan.transport.javacoap.request.ResponseCodeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.packet.CoapResponse; +import com.mbed.coap.packet.Code; +import com.mbed.coap.packet.MediaTypes; +import com.mbed.coap.packet.Opaque; +import com.mbed.coap.transport.TransportContext; +import com.mbed.coap.utils.Service; + +public abstract class LwM2mCoapResource implements Service { + + private static final Logger LOG = LoggerFactory.getLogger(LwM2mCoapResource.class); + + private final String uri; + + public LwM2mCoapResource(String uri) { + this.uri = uri; + } + + @Override + public CompletableFuture apply(CoapRequest coapRequest) { + try { + // The LWM2M transport spec v1.1.1 (section 6.4) all operation must be confirmable message except notify and + // execute which may be NON + if (coapRequest.getTransContext().get(TransportContext.NON_CONFIRMABLE)) { + return handleInvalidRequest(coapRequest, "CON CoAP type expected"); + } + + return handleRequest(coapRequest); + } catch (InvalidRequestException e) { + return handleInvalidRequest(coapRequest, e.getMessage(), e); + } catch (RuntimeException e) { + LOG.error("Exception while handling request [{}] on the resource {} from {}", coapRequest, getURI(), + extractIdentitySafely(coapRequest), e); + return completedFuture(CoapResponse.of(Code.C500_INTERNAL_SERVER_ERROR)); + } + } + + protected CompletableFuture handleRequest(CoapRequest coapRequest) { + switch (coapRequest.getMethod()) { + case GET: + return handleGET(coapRequest); + case POST: + return handlePOST(coapRequest); + case PUT: + return handlePUT(coapRequest); + case DELETE: + return handleDELETE(coapRequest); + case FETCH: + return handleFETCH(coapRequest); + case PATCH: + return handlePATCH(coapRequest); + case iPATCH: + return handleIPATCH(coapRequest); + default: + return completedFuture(CoapResponse.of(Code.C500_INTERNAL_SERVER_ERROR, + String.format("supported Method %s", coapRequest.getMethod()))); + } + } + + protected CompletableFuture handleDELETE(CoapRequest coapRequest) { + return completedFuture(CoapResponse.of(Code.C405_METHOD_NOT_ALLOWED)); + } + + protected CompletableFuture handlePUT(CoapRequest coapRequest) { + return completedFuture(CoapResponse.of(Code.C405_METHOD_NOT_ALLOWED)); + } + + protected CompletableFuture handlePOST(CoapRequest coapRequest) { + return completedFuture(CoapResponse.of(Code.C405_METHOD_NOT_ALLOWED)); + } + + protected CompletableFuture handleGET(CoapRequest coapRequest) { + return completedFuture(CoapResponse.of(Code.C405_METHOD_NOT_ALLOWED)); + } + + protected CompletableFuture handleFETCH(CoapRequest coapRequest) { + return completedFuture(CoapResponse.of(Code.C405_METHOD_NOT_ALLOWED)); + } + + protected CompletableFuture handlePATCH(CoapRequest coapRequest) { + return completedFuture(CoapResponse.of(Code.C405_METHOD_NOT_ALLOWED)); + } + + protected CompletableFuture handleIPATCH(CoapRequest coapRequest) { + return completedFuture(CoapResponse.of(Code.C405_METHOD_NOT_ALLOWED)); + } + + protected String getURI() { + return uri; + } + + protected IpPeer getForeignPeerIdentity(CoapRequest coapRequest) { + return new IpPeer(coapRequest.getPeerAddress()); + } + + protected IpPeer extractIdentitySafely(CoapRequest coapRequest) { + try { + return getForeignPeerIdentity(coapRequest); + } catch (RuntimeException e) { + LOG.error("Unable to extract identity", e); + return null; + } + } + + /** + * Handle an Invalid Request by sending a BAD_REQUEST response and logging the error using debug level. + * + * @param coapRequest The invalid CoAP request + * @param message The error message describing why the request is invalid. + */ + protected CompletableFuture handleInvalidRequest(CoapRequest coapRequest, String message) { + return handleInvalidRequest(coapRequest, message, null); + } + + /** + * Handle an Invalid Request by sending a BAD_REQUEST response and logging the error using debug level. + * + * @param coapRequest The invalid CoAP request + * @param message The error message describing why the request is invalid. + * @param error An {@link Throwable} raised while we handle try create a LWM2M request from CoAP request. + */ + protected CompletableFuture handleInvalidRequest(CoapRequest coapRequest, String message, + Throwable error) { + + // Log error + if (LOG.isDebugEnabled()) { + if (error != null) { + LOG.debug("Invalid request [{}] received on the resource rd from {}", coapRequest, getURI(), + extractIdentitySafely(coapRequest), error); + } else { + LOG.debug("Invalid request [{}] received on the resource rd from {} : {}", coapRequest, getURI(), + extractIdentitySafely(coapRequest), message); + } + } + + CoapResponse coapResponse = CoapResponse.of(Code.C400_BAD_REQUEST); + if (message != null) { + coapResponse.payload(Opaque.of(message)); + coapResponse.options().setContentFormat(MediaTypes.CT_TEXT_PLAIN); + } + return completedFuture(coapResponse); + } + + protected CompletableFuture errorMessage(ResponseCode errorCode, String message) { + CoapResponse coapResponse = CoapResponse.of(ResponseCodeUtil.toCoapResponseCode(errorCode)); + if (message != null) { + coapResponse = coapResponse.payload(Opaque.of(message)); + coapResponse.options().setContentFormat(MediaTypes.CT_TEXT_PLAIN); + } + return completedFuture(coapResponse); + } + + public CompletableFuture responseWithPayload(ResponseCode code, ContentFormat format, + byte[] payload) { + return completedFuture(CoapResponse.of( // + ResponseCodeUtil.toCoapResponseCode(code), // + Opaque.of(payload), // + (short) format.getCode())); + } + + public CompletableFuture emptyResponse(ResponseCode code) { + return completedFuture(CoapResponse.of(ResponseCodeUtil.toCoapResponseCode(code))); + } + + protected List getUriPart(CoapRequest coapRequest) { + String uriAsString = coapRequest.options().getUriPath(); + if (uriAsString == null) { + return null; + } + // remove first '/' + if (uriAsString.startsWith("/")) { + uriAsString = uriAsString.substring(1); + } + List uri = Arrays.asList(uriAsString.split("/")); + return uri; + } +} diff --git a/leshan-tl-javacoap-server/logback-leshan-test.xml b/leshan-tl-javacoap-server/logback-leshan-test.xml new file mode 100644 index 0000000000..2e93285e57 --- /dev/null +++ b/leshan-tl-javacoap-server/logback-leshan-test.xml @@ -0,0 +1,35 @@ + + + + + + + %d %p %C{1} [%t] %m%n + + + + + + + + diff --git a/leshan-tl-javacoap-server/pom.xml b/leshan-tl-javacoap-server/pom.xml new file mode 100644 index 0000000000..b199c8eee0 --- /dev/null +++ b/leshan-tl-javacoap-server/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + org.eclipse.leshan + lib-build-config + 2.0.0-SNAPSHOT + ../build-config/lib-build-config/pom.xml + + leshan-tl-javacoap-server + bundle + leshan - server java-coap + A transport implementation for leshan server based on Java CoAP + + + + org.eclipse.leshan + leshan-server-core + + + org.eclipse.leshan + leshan-tl-javacoap-core + + + diff --git a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/EndpointUriProvider.java b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/EndpointUriProvider.java new file mode 100644 index 0000000000..f2b4c8b091 --- /dev/null +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/EndpointUriProvider.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.endpoint; + +import java.net.URI; + +import org.eclipse.leshan.core.endpoint.EndpointUriUtil; +import org.eclipse.leshan.core.endpoint.Protocol; + +import com.mbed.coap.server.CoapServer; + +public class EndpointUriProvider { + + private CoapServer coapServer; + private final Protocol protocol; + + public EndpointUriProvider(Protocol protocol) { + this.protocol = protocol; + } + + public void setCoapServer(CoapServer coapServer) { + this.coapServer = coapServer; + } + + public URI getEndpointUri() { + return EndpointUriUtil.createUri(protocol.getUriScheme(), coapServer.getLocalSocketAddress()); + } +} diff --git a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/JavaCoapServerEndpoint.java b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/JavaCoapServerEndpoint.java new file mode 100644 index 0000000000..7d53d0d832 --- /dev/null +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/JavaCoapServerEndpoint.java @@ -0,0 +1,243 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.endpoint; + +import java.net.URI; +import java.util.SortedMap; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; + +import org.eclipse.leshan.core.endpoint.EndpointUriUtil; +import org.eclipse.leshan.core.endpoint.Protocol; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.request.DownlinkRequest; +import org.eclipse.leshan.core.request.exception.RequestCanceledException; +import org.eclipse.leshan.core.request.exception.SendFailedException; +import org.eclipse.leshan.core.request.exception.TimeoutException.Type; +import org.eclipse.leshan.core.response.ErrorCallback; +import org.eclipse.leshan.core.response.LwM2mResponse; +import org.eclipse.leshan.core.response.ResponseCallback; +import org.eclipse.leshan.core.util.NamedThreadFactory; +import org.eclipse.leshan.core.util.Validate; +import org.eclipse.leshan.server.endpoint.LwM2mServerEndpoint; +import org.eclipse.leshan.server.endpoint.ServerEndpointToolbox; +import org.eclipse.leshan.server.profile.ClientProfile; +import org.eclipse.leshan.server.request.LowerLayerConfig; + +import com.mbed.coap.exception.CoapTimeoutException; +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.packet.CoapResponse; +import com.mbed.coap.server.CoapServer; + +public class JavaCoapServerEndpoint implements LwM2mServerEndpoint { + + private final CoapServer coapServer; + private final ServerCoapMessageTranslator translator; + private final ServerEndpointToolbox toolbox; + + private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1, + new NamedThreadFactory("Leshan Async Request timeout")); + + // A map which contains all ongoing CoAP requests + // This is used to be able to cancel request + private final ConcurrentNavigableMap< // + String, // sessionId#requestId + CompletableFuture> // future of the ongoing Coap Request + ongoingRequests = new ConcurrentSkipListMap<>(); + + public JavaCoapServerEndpoint(CoapServer coapServer, ServerCoapMessageTranslator translator, + ServerEndpointToolbox toolbox) { + this.coapServer = coapServer; + this.translator = translator; + this.toolbox = toolbox; + } + + @Override + public Protocol getProtocol() { + return Protocol.COAP; + } + + @Override + public String getDescription() { + return "CoAP over UDP based on java-coap library"; + } + + @Override + public URI getURI() { + return EndpointUriUtil.createUri(getProtocol().getUriScheme(), coapServer.getLocalSocketAddress()); + } + + @Override + public T send(ClientProfile destination, DownlinkRequest request, + LowerLayerConfig lowerLayerConfig, long timeoutInMs) throws InterruptedException { + + // Send LWM2M Request + CompletableFuture lwm2mResponseFuture = sendLwM2mRequest(destination, request, lowerLayerConfig); + + // Wait synchronously for LWM2M response + try { + return lwm2mResponseFuture.get(timeoutInMs, TimeUnit.MILLISECONDS); + } catch (CompletionException | ExecutionException | CancellationException exception) { + if (lwm2mResponseFuture.isCancelled()) { + throw new RequestCanceledException(); + } else { + if (exception.getCause() instanceof CoapTimeoutException) { + return null; + } else { + throw new SendFailedException("Unable to send request " + exception.getCause(), exception); + } + } + } catch (TimeoutException e) { + lwm2mResponseFuture.cancel(true); + return null; + } + } + + @Override + public void send(ClientProfile destination, DownlinkRequest request, + ResponseCallback responseCallback, ErrorCallback errorCallback, LowerLayerConfig lowerLayerConfig, + long timeoutInMs) { + + // Send LWM2M Request + CompletableFuture lwm2mResponseFuture = sendLwM2mRequest(destination, request, lowerLayerConfig); + + // Attach callback + lwm2mResponseFuture.whenComplete((lwM2mResponse, exception) -> { + // Handle Exception + if (exception != null) { + if (exception instanceof CancellationException) { + errorCallback.onError(new RequestCanceledException()); + } else if (exception instanceof TimeoutException) { + errorCallback.onError(new org.eclipse.leshan.core.request.exception.TimeoutException( + Type.RESPONSE_TIMEOUT, exception.getCause(), "LWM2M response Timeout")); + } else if (exception instanceof CompletionException + && exception.getCause() instanceof CoapTimeoutException) { + errorCallback.onError(new org.eclipse.leshan.core.request.exception.TimeoutException( + Type.COAP_TIMEOUT, exception.getCause(), "Coap Timeout")); + } else { + errorCallback.onError(new SendFailedException("Unable to send request " + exception.getCause(), + exception.getCause())); + } + } else { + // Handle CoAP Response + responseCallback.onResponse(lwM2mResponse); + } + }); + + // Handle timeout + timeoutAfter(lwm2mResponseFuture, timeoutInMs); + } + + protected CompletableFuture sendLwM2mRequest(ClientProfile destination, + DownlinkRequest lwm2mRequest, LowerLayerConfig lowerLayerConfig) { + + CompletableFuture lwm2mResponseFuture; + // Create Coap Request to send from LWM2M Request + CoapRequest coapRequest = translator.createCoapRequest(destination, lwm2mRequest, toolbox); + + // Apply Users customization + applyUserConfig(lowerLayerConfig, coapRequest); + + // Send CoAP Request + CompletableFuture coapResponseFuture = coapServer.clientService().apply(coapRequest); + + // On response, create LWM2M Response from CoAP response + lwm2mResponseFuture = coapResponseFuture.thenApply(coapResponse -> translator.createLwM2mResponse(destination, + lwm2mRequest, coapResponse, coapRequest, toolbox)); + + // store ongoing request + addOngoingRequest(destination.getRegistrationId(), lwm2mResponseFuture); + + return lwm2mResponseFuture; + } + + public void timeoutAfter(CompletableFuture future, long timeoutInMs) { + // schedule a timeout task to stop future after given amount of time + ScheduledFuture timeoutTask = executor.schedule(() -> { + if (future != null && !future.isDone()) { + future.completeExceptionally(new TimeoutException()); + } + }, timeoutInMs, TimeUnit.MILLISECONDS); + + future.whenComplete((r, e) -> { + // Cancel TimeoutTask just above when future is complete + if (e == null && timeoutTask != null && !timeoutTask.isDone()) { + timeoutTask.cancel(false); + } + }); + } + + @Override + public void cancelRequests(String sessionID) { + Validate.notNull(sessionID); + SortedMap> requests = ongoingRequests + .subMap(getFloorKey(sessionID), getCeilingKey(sessionID)); + for (CompletableFuture request : requests.values()) { + request.cancel(false); + } + requests.clear(); + } + + @Override + public void cancelObservation(Observation observation) { + // TODO not sure there is something to implement here. + // Maybe trying to cancel ongoing observe request linked to this observation ? + } + + private static String getFloorKey(String sessionID) { + // The key format is sessionid#long, So we need a key which is always before this pattern (in natural order). + return sessionID + '#'; + } + + private static String getCeilingKey(String sessionID) { + // The key format is sessionid#long, So we need a key which is always after this pattern (in natural order). + return sessionID + "#A"; + } + + private static String getKey(String sessionID, long requestId) { + return sessionID + '#' + requestId; + } + + private final AtomicLong idGenerator = new AtomicLong(0l); + + private void addOngoingRequest(String sessionID, CompletableFuture coapRequest) { + if (sessionID != null) { + final String key = getKey(sessionID, idGenerator.incrementAndGet()); + ongoingRequests.put(key, coapRequest); + coapRequest.whenComplete((r, e) -> { + ongoingRequests.remove(key); + }); + } + } + + private void applyUserConfig(LowerLayerConfig lowerLayerConfig, CoapRequest request) { + // TODO This is probably not so useful because of + // https://github.com/open-coap/java-coap/issues/27#issuecomment-1514790233 + // But with should help to adapt the code : https://github.com/open-coap/java-coap/pull/68 + if (lowerLayerConfig != null) + lowerLayerConfig.apply(request); + } +} diff --git a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/JavaCoapServerEndpointsProvider.java b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/JavaCoapServerEndpointsProvider.java new file mode 100644 index 0000000000..94c21b5ddf --- /dev/null +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/JavaCoapServerEndpointsProvider.java @@ -0,0 +1,138 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.endpoint; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.eclipse.leshan.core.endpoint.Protocol; +import org.eclipse.leshan.server.LeshanServer; +import org.eclipse.leshan.server.endpoint.LwM2mServerEndpoint; +import org.eclipse.leshan.server.endpoint.LwM2mServerEndpointsProvider; +import org.eclipse.leshan.server.endpoint.ServerEndpointToolbox; +import org.eclipse.leshan.server.observation.LwM2mNotificationReceiver; +import org.eclipse.leshan.server.request.UplinkRequestReceiver; +import org.eclipse.leshan.server.security.ServerSecurityInfo; +import org.eclipse.leshan.transport.javacoap.server.observation.CoapNotificationReceiver; +import org.eclipse.leshan.transport.javacoap.server.observation.LwM2mObservationsStore; +import org.eclipse.leshan.transport.javacoap.server.resource.RegistrationResource; +import org.eclipse.leshan.transport.javacoap.server.resource.SendResource; + +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.packet.CoapResponse; +import com.mbed.coap.server.CoapServer; +import com.mbed.coap.server.CoapServerBuilder; +import com.mbed.coap.server.RouterService; +import com.mbed.coap.server.filter.TokenGeneratorFilter; +import com.mbed.coap.transport.udp.DatagramSocketTransport; +import com.mbed.coap.utils.Service; + +public class JavaCoapServerEndpointsProvider implements LwM2mServerEndpointsProvider { + + private CoapServer coapServer; + private final InetSocketAddress localAddress; + private JavaCoapServerEndpoint lwm2mEndpoint; + + public JavaCoapServerEndpointsProvider(InetSocketAddress localAddress) { + this.localAddress = localAddress; + } + + @Override + public void createEndpoints(UplinkRequestReceiver requestReceiver, + final LwM2mNotificationReceiver notificationReceiver, final ServerEndpointToolbox toolbox, + ServerSecurityInfo serverSecurityInfo, LeshanServer server) { + + // TODO: HACK to be able to get local URI in resource, need to discuss about it with java-coap. + EndpointUriProvider endpointUriProvider = new EndpointUriProvider(Protocol.COAP); + + // Create Resources / Routes + RegistrationResource registerResource = new RegistrationResource(requestReceiver, toolbox.getLinkParser(), + endpointUriProvider); + Service resources = RouterService.builder() // + .any("/rd/*", registerResource) // + .any("/rd", registerResource)// + .any("/dp", + new SendResource(requestReceiver, toolbox.getDecoder(), toolbox.getProfileProvider(), + endpointUriProvider))// + .build(); + + // Create CoAP Server + coapServer = createCoapServer() // + .transport(new DatagramSocketTransport(localAddress)) // + .route(resources) // + .notificationsReceiver(new CoapNotificationReceiver(coapServer, notificationReceiver, + server.getRegistrationStore(), server.getModelProvider(), toolbox.getDecoder())) // + .observationsStore(new LwM2mObservationsStore(server.getRegistrationStore(), notificationReceiver)) // + .build(); + endpointUriProvider.setCoapServer(coapServer); + + lwm2mEndpoint = new JavaCoapServerEndpoint(coapServer, new ServerCoapMessageTranslator(), toolbox); + + } + + protected CoapServerBuilder createCoapServer() { + return CoapServer.builder().outboundFilter(TokenGeneratorFilter.RANDOM); + } + + @Override + public List getEndpoints() { + // java-coap CoapServer support only 1 socket/endpoint by server. + // So for now this endpoint provider support only 1 endpoint. + // If we want to support more, we need to : + // - either create serveral coap server by provider + // - or create a kind or custom transport proxy with several transport. + if (lwm2mEndpoint == null) { + return Collections.emptyList(); + } else { + return Arrays.asList(lwm2mEndpoint); + } + } + + @Override + public LwM2mServerEndpoint getEndpoint(URI uri) { + if (lwm2mEndpoint != null && lwm2mEndpoint.getURI().equals(uri)) + return lwm2mEndpoint; + else + return null; + } + + @Override + public void start() { + try { + coapServer.start(); + } catch (IOException e) { + throw new IllegalStateException("Unable to start java-coap endpoint", e); + } + } + + @Override + public void stop() { + // TODO in Leshan stop means "we can restart after a stop" + // but in java-coap : There is no restart after stop, need to create new instance to start again. + // I don't know if we should remove stop from Leshan API ? + coapServer.stop(); + } + + @Override + public void destroy() { + // TODO there is no destroy, so we just stop ? + coapServer.stop(); + } +} diff --git a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/ServerCoapMessageTranslator.java b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/ServerCoapMessageTranslator.java new file mode 100644 index 0000000000..2c2ec89845 --- /dev/null +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/endpoint/ServerCoapMessageTranslator.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.endpoint; + +import org.eclipse.leshan.core.request.DownlinkRequest; +import org.eclipse.leshan.core.response.LwM2mResponse; +import org.eclipse.leshan.server.endpoint.ServerEndpointToolbox; +import org.eclipse.leshan.server.profile.ClientProfile; +import org.eclipse.leshan.transport.javacoap.server.request.CoapRequestBuilder; +import org.eclipse.leshan.transport.javacoap.server.request.LwM2mResponseBuilder; + +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.packet.CoapResponse; + +public class ServerCoapMessageTranslator { + + public CoapRequest createCoapRequest(ClientProfile clientProfile, + DownlinkRequest lwm2mRequest, + ServerEndpointToolbox toolbox /* , IdentityHandler identityHandler */) { + + CoapRequestBuilder builder = new CoapRequestBuilder(clientProfile.getRegistration(), + clientProfile.getTransportData(), clientProfile.getRootPath(), clientProfile.getModel(), + toolbox.getEncoder()); + lwm2mRequest.accept(builder); + return builder.getRequest(); + } + + public T createLwM2mResponse(ClientProfile clientProfile, DownlinkRequest lwm2mRequest, + CoapResponse coapResponse, CoapRequest coapRequest, ServerEndpointToolbox toolbox) { + + LwM2mResponseBuilder builder = new LwM2mResponseBuilder(coapResponse, coapRequest, + clientProfile.getEndpoint(), clientProfile.getModel(), toolbox.getDecoder(), toolbox.getLinkParser()); + lwm2mRequest.accept(builder); + return builder.getResponse(); + } +} 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 new file mode 100644 index 0000000000..9fef24a4a6 --- /dev/null +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/CoapNotificationReceiver.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.observation; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +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.TimestampedLwM2mNode; +import org.eclipse.leshan.core.node.codec.LwM2mDecoder; +import org.eclipse.leshan.core.observation.CompositeObservation; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.ObservationIdentifier; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.peer.IpPeer; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.response.AbstractLwM2mResponse; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; +import org.eclipse.leshan.core.response.ObserveResponse; +import org.eclipse.leshan.server.model.LwM2mModelProvider; +import org.eclipse.leshan.server.observation.LwM2mNotificationReceiver; +import org.eclipse.leshan.server.profile.ClientProfile; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.registration.RegistrationStore; +import org.eclipse.leshan.transport.javacoap.request.ResponseCodeUtil; + +import com.mbed.coap.packet.CoapResponse; +import com.mbed.coap.packet.Opaque; +import com.mbed.coap.packet.SeparateResponse; +import com.mbed.coap.server.CoapServer; +import com.mbed.coap.server.observe.NotificationsReceiver; + +public class CoapNotificationReceiver implements NotificationsReceiver { + + private final CoapServer coapServer; + private final LwM2mNotificationReceiver notificationReceiver; + private final RegistrationStore registrationStore; + private final LwM2mModelProvider modelProvider; + private final LwM2mDecoder decoder; + + public CoapNotificationReceiver(CoapServer coapServer, LwM2mNotificationReceiver notificationReceiver, + RegistrationStore registrationStore, LwM2mModelProvider modelProvider, LwM2mDecoder decoder) { + super(); + this.coapServer = coapServer; + this.notificationReceiver = notificationReceiver; + this.registrationStore = registrationStore; + this.modelProvider = modelProvider; + this.decoder = decoder; + } + + @Override + public boolean onObservation(String resourceUriPath, SeparateResponse coapResponse) { + // Get foreign peer data from separated response + InetSocketAddress peerAddress = coapResponse.getPeerAddress(); + IpPeer sender = new IpPeer(peerAddress); + + // Search if there is an observation for this resource. + ObservationIdentifier observationId = new ObservationIdentifier(coapResponse.getToken().getBytes()); + final Observation observation = registrationStore.getObservation(observationId); + if (observation == null) + return false; + + // Check if path is the right one. + Optional observationPath = ObservationUtil.getPath(observation); // + if (!observationPath.filter(p -> p.equals(resourceUriPath)).isPresent()) { + throw new IllegalStateException(String.format("Observation path %s does not match reponse path %s ", + observationPath.orElse("null"), resourceUriPath)); + } + + // In case of block transfer, call to retrieve rest of payload. + CompletableFuture payload = NotificationsReceiver.retrieveRemainingBlocks(resourceUriPath, coapResponse, + req -> coapServer.clientService().apply(req)); + + // Handle CoAP Notification + payload.whenComplete((p, e) -> { + // Check we have a corresponding registration + Registration registration = registrationStore.getRegistration(observation.getRegistrationId()); + if (registration == null) { + throw new IllegalStateException( + String.format("No registration with Id %s", observation.getRegistrationId())); + } + + // Create Client Profile + ClientProfile clientProfile = new ClientProfile(registration, modelProvider.getObjectModel(registration)); + try { + // Send events + if (e != null) { + // on Error + // TODO should we stop observe relation ? + notificationReceiver.onError(observation, sender, clientProfile, + e instanceof Exception ? (Exception) e : new Exception(e)); + } else if (p != null) { + AbstractLwM2mResponse observeResponse = createLwM2mResponseForNotification(observation, + coapResponse.asResponse(), clientProfile); + if (observation instanceof SingleObservation) { + + // Single Observe Notification + notificationReceiver.onNotification((SingleObservation) observation, sender, clientProfile, + (ObserveResponse) observeResponse); + } else if (observation instanceof CompositeObservation) { + + // Composite Observe Notification + notificationReceiver.onNotification((CompositeObservation) observation, sender, clientProfile, + (ObserveCompositeResponse) observeResponse); + } else { + throw new IllegalStateException(String.format("Unexpected observation : %s is not supported", + observation.getClass().getSimpleName())); + } + } else { + throw new IllegalStateException("unexpected behavior when handling notification"); + } + } catch (Exception exception) { + // TODO should we stop observe relation ? + notificationReceiver.onError(observation, sender, clientProfile, exception); + } + }); + + return true; + } + + public AbstractLwM2mResponse createLwM2mResponseForNotification(Observation observation, CoapResponse coapResponse, + ClientProfile profile) { + + ResponseCode responseCode = ResponseCodeUtil.toLwM2mResponseCode(coapResponse.getCode()); + + if (observation instanceof SingleObservation) { + SingleObservation singleObservation = (SingleObservation) observation; + + ContentFormat contentFormat = ContentFormat.fromCode(coapResponse.options().getContentFormat()); + List timestampedNodes = decoder.decodeTimestampedData( + coapResponse.getPayload().getBytes(), contentFormat, singleObservation.getPath(), + profile.getModel()); + + // create lwm2m response + if (timestampedNodes.size() == 1 && !timestampedNodes.get(0).isTimestamped()) { + return new ObserveResponse(responseCode, timestampedNodes.get(0).getNode(), null, singleObservation, + null, coapResponse); + } else { + return new ObserveResponse(responseCode, null, timestampedNodes, singleObservation, null, coapResponse); + } + } else if (observation instanceof CompositeObservation) { + 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); + } + return null; + } + +} diff --git a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/LwM2mKeys.java b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/LwM2mKeys.java new file mode 100644 index 0000000000..46cfa5b407 --- /dev/null +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/LwM2mKeys.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.observation; + +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.server.registration.Registration; + +import com.mbed.coap.transport.TransportContext.Key; + +public class LwM2mKeys { + + // Keys for Observe Request + public static final Key LESHAN_OBSERVATION = new Key<>(null); + public static final Key LESHAN_REGISTRATION = new Key<>(null); +} diff --git a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/LwM2mObservationsStore.java b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/LwM2mObservationsStore.java new file mode 100644 index 0000000000..ae83c7d735 --- /dev/null +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/LwM2mObservationsStore.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.observation; + +import java.util.Collection; +import java.util.Optional; + +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.ObservationIdentifier; +import org.eclipse.leshan.core.peer.LwM2mIdentity; +import org.eclipse.leshan.core.peer.SocketIdentity; +import org.eclipse.leshan.server.observation.LwM2mNotificationReceiver; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.registration.RegistrationStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.packet.SeparateResponse; +import com.mbed.coap.server.observe.ObservationsStore; + +public class LwM2mObservationsStore implements ObservationsStore { + private static final Logger LOG = LoggerFactory.getLogger(LwM2mObservationsStore.class); + + private final RegistrationStore store; + private final LwM2mNotificationReceiver notificationReceiver; + + public LwM2mObservationsStore(RegistrationStore store, LwM2mNotificationReceiver notificationReceiver) { + this.store = store; + this.notificationReceiver = notificationReceiver; + } + + @Override + public void add(CoapRequest obsReq) { + Observation observation = obsReq.getTransContext(LwM2mKeys.LESHAN_OBSERVATION); + if (observation == null) { + String errMessage = "missing LESHAN_OBSERVATION key in coap request transport context"; + LOG.warn(errMessage); + throw new IllegalStateException(errMessage); + } + + Registration registration = obsReq.getTransContext(LwM2mKeys.LESHAN_REGISTRATION); + if (registration == null) { + String errMessage = "missing LESHAN_REGISTRATION key in coap request transport context"; + LOG.warn(errMessage); + throw new IllegalStateException(errMessage); + } + + Collection removed = null; + try { + removed = store.addObservation(registration.getId(), observation, false); + } catch (Exception e) { + LOG.warn("Unable to add observation {}", observation, e); + throw e; + } + + // Manage cancellation + if (removed != null && !removed.isEmpty()) { + for (Observation obsRemoved : removed) { + notificationReceiver.cancelled(obsRemoved); + } + } + notificationReceiver.newObservation(observation, registration); + } + + @Override + public Optional resolveUriPath(SeparateResponse obs) { + // Try to find observation for given token + ObservationIdentifier observationIdentifier = new ObservationIdentifier(obs.getToken().getBytes()); + Observation observation = store.getObservation(observationIdentifier); + + // TODO should we use PeerIdentity in ObservationIdentifier. + if (observation == null) + return Optional.empty(); + Registration registration = store.getRegistration(observation.getRegistrationId()); + if (registration == null) + return Optional.empty(); + LwM2mIdentity identity = registration.getClientTransportData().getIdentity(); + if (!(identity instanceof SocketIdentity)) + return Optional.empty(); + if (!((SocketIdentity) identity).getSocketAddress().equals(obs.getPeerAddress())) + return Optional.empty(); + + return ObservationUtil.getPath(observation); + } + + @Override + public void remove(SeparateResponse obs) { + // Try to find observation for given token + ObservationIdentifier observationIdentifier = new ObservationIdentifier(obs.getToken().getBytes()); + Observation observation = store.getObservation(observationIdentifier); + + if (observation != null) { + // try to remove observation + Observation removedObservation = store.removeObservation(observation.getRegistrationId(), + observationIdentifier); + if (removedObservation != null) { + notificationReceiver.cancelled(removedObservation); + } + } + } +} diff --git a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/ObservationUtil.java b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/ObservationUtil.java new file mode 100644 index 0000000000..3c0fdbf49c --- /dev/null +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/observation/ObservationUtil.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.observation; + +import java.util.Optional; + +import org.eclipse.leshan.core.observation.CompositeObservation; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; + +public class ObservationUtil { + + // TODO should we support rootpath ? + public static Optional getPath(Observation observation) { + if (observation instanceof SingleObservation) { + return Optional.of(((SingleObservation) observation).getPath().toString()); + } else if (observation instanceof CompositeObservation) { + return Optional.of("/"); + } else if (observation == null) { + return Optional.empty(); + } else { + throw new IllegalStateException(String.format("Unexpected kind of observation : %s is not supported", + observation.getClass().getSimpleName())); + } + } +} diff --git a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/CoapRequestBuilder.java b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/CoapRequestBuilder.java new file mode 100644 index 0000000000..283806a6c0 --- /dev/null +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/CoapRequestBuilder.java @@ -0,0 +1,298 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.request; + +import java.net.InetSocketAddress; +import java.util.Collections; + +import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.node.LwM2mIncompletePath; +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.codec.LwM2mEncoder; +import org.eclipse.leshan.core.observation.CompositeObservation; +import org.eclipse.leshan.core.observation.ObservationIdentifier; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.peer.IpPeer; +import org.eclipse.leshan.core.peer.LwM2mPeer; +import org.eclipse.leshan.core.request.BootstrapDeleteRequest; +import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; +import org.eclipse.leshan.core.request.BootstrapFinishRequest; +import org.eclipse.leshan.core.request.BootstrapReadRequest; +import org.eclipse.leshan.core.request.BootstrapWriteRequest; +import org.eclipse.leshan.core.request.CancelCompositeObservationRequest; +import org.eclipse.leshan.core.request.CancelObservationRequest; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.CreateRequest; +import org.eclipse.leshan.core.request.DeleteRequest; +import org.eclipse.leshan.core.request.DiscoverRequest; +import org.eclipse.leshan.core.request.DownlinkRequest; +import org.eclipse.leshan.core.request.DownlinkRequestVisitor; +import org.eclipse.leshan.core.request.ExecuteRequest; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; +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.WriteAttributesRequest; +import org.eclipse.leshan.core.request.WriteCompositeRequest; +import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.transport.javacoap.request.RandomTokenGenerator; +import org.eclipse.leshan.transport.javacoap.server.observation.LwM2mKeys; + +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.packet.MediaTypes; +import com.mbed.coap.packet.Opaque; +import com.mbed.coap.transport.TransportContext; + +/** + * This class is able to create CoAP request from LWM2M {@link DownlinkRequest}. + *

+ * Call CoapRequestBuilder#visit(lwm2mRequest), then get the result using {@link #getRequest()} + */ +public class CoapRequestBuilder implements DownlinkRequestVisitor { + + private CoapRequest coapRequest; + + // client information + private final LwM2mPeer destination; + private final Registration registration; + private final String rootPath; + private final LwM2mEncoder encoder; + private final LwM2mModel model; + // TODO we should better manage this and especially better handle token conflict + private final RandomTokenGenerator tokenGenerator = new RandomTokenGenerator(8); + + public CoapRequestBuilder(Registration registration, LwM2mPeer destination, String rootPath, LwM2mModel model, + LwM2mEncoder encoder) { + this.registration = registration; + this.destination = destination; + this.rootPath = rootPath; + this.model = model; + this.encoder = encoder; + } + + @Override + public void visit(ReadRequest request) { + coapRequest = CoapRequest.get(getURI(request.getPath())).address(getAddress()); + if (request.getContentFormat() != null) + coapRequest.options().setAccept(request.getContentFormat().getCode()); + } + + @Override + public void visit(DiscoverRequest request) { + coapRequest = CoapRequest.get(getURI(request.getPath())).address(getAddress()); + coapRequest.options().setAccept(MediaTypes.CT_APPLICATION_LINK__FORMAT); + } + + @Override + public void visit(WriteRequest request) { + coapRequest = request.isReplaceRequest() ? CoapRequest.put(getURI(request.getPath())).address(getAddress()) + : CoapRequest.post(getURI(request.getPath())).address(getAddress()); + ContentFormat format = request.getContentFormat(); + coapRequest.options().setContentFormat((short) format.getCode()); + coapRequest = coapRequest + .payload(Opaque.of(encoder.encode(request.getNode(), format, request.getPath(), model))); + } + + @Override + public void visit(WriteAttributesRequest request) { + coapRequest = CoapRequest.put(getURI(request.getPath())).address(getAddress()); + coapRequest.options().setUriQuery(request.getAttributes().toString()); + } + + @Override + public void visit(ExecuteRequest request) { + coapRequest = CoapRequest.post(getURI(request.getPath())).address(getAddress()); + String payload = request.getArguments().serialize(); + if (payload != null) { + coapRequest.payload(payload); + coapRequest.options().setContentFormat(MediaTypes.CT_TEXT_PLAIN); + } + } + + @Override + public void visit(CreateRequest request) { + coapRequest = CoapRequest.post(getURI(request.getPath())).address(getAddress()); + coapRequest.options().setContentFormat((short) request.getContentFormat().getCode()); + // if no instance id, the client will assign it. + LwM2mNode node; + if (request.unknownObjectInstanceId()) { + node = new LwM2mObjectInstance(request.getResources()); + } else { + node = new LwM2mObject(request.getPath().getObjectId(), request.getObjectInstances()); + } + coapRequest = coapRequest + .payload(Opaque.of(encoder.encode(node, request.getContentFormat(), request.getPath(), model))); + } + + @Override + public void visit(DeleteRequest request) { + coapRequest = CoapRequest.delete(getURI(request.getPath())).address(getAddress()); + } + + @Override + public void visit(ObserveRequest request) { + coapRequest = CoapRequest.observe(getAddress(), getURI(request.getPath())); + if (request.getContentFormat() != null) + coapRequest.options().setAccept(request.getContentFormat().getCode()); + + // Create Observation + // TODO the token generation is probably an issue : + // What happens in case of conflict but also how could we follow : + // https://www.rfc-editor.org/rfc/rfc9175#section-4.2 + Opaque token = tokenGenerator.createToken(); + SingleObservation observation = new SingleObservation(new ObservationIdentifier(token.getBytes()), + registration.getId(), request.getPath(), request.getContentFormat(), request.getContext(), + Collections.emptyMap()); + + // Add Observation to request context + TransportContext extendedContext = coapRequest.getTransContext() // + .with(LwM2mKeys.LESHAN_OBSERVATION, observation) // + .with(LwM2mKeys.LESHAN_REGISTRATION, registration); + coapRequest = coapRequest.context(extendedContext); + coapRequest = coapRequest.token(token); + } + + @Override + public void visit(CancelObservationRequest request) { + + coapRequest = CoapRequest.observe(getAddress(), getURI(request.getPath())) + .token(Opaque.of(request.getObservation().getId().getBytes())); + coapRequest.observe(1); + if (request.getContentFormat() != null) + coapRequest.options().setAccept(request.getContentFormat().getCode()); + } + + @Override + public void visit(ReadCompositeRequest request) { + coapRequest = CoapRequest.fetch(getURI(LwM2mPath.ROOTPATH)).address(getAddress()); + coapRequest.options().setContentFormat((short) request.getRequestContentFormat().getCode()); + coapRequest = coapRequest + .payload(Opaque.of(encoder.encodePaths(request.getPaths(), request.getRequestContentFormat()))); + if (request.getResponseContentFormat() != null) { + coapRequest.options().setAccept(request.getResponseContentFormat().getCode()); + } + } + + @Override + public void visit(ObserveCompositeRequest request) { + coapRequest = CoapRequest.fetch(getURI(LwM2mPath.ROOTPATH)).address(getAddress()); + coapRequest.options().setContentFormat((short) request.getRequestContentFormat().getCode()); + coapRequest = coapRequest + .payload(Opaque.of(encoder.encodePaths(request.getPaths(), request.getRequestContentFormat()))); + if (request.getResponseContentFormat() != null) { + coapRequest.options().setAccept(request.getResponseContentFormat().getCode()); + } + coapRequest.options().setObserve(0); + + // Create Observation + Opaque token = tokenGenerator.createToken(); + CompositeObservation observation = new CompositeObservation(new ObservationIdentifier(token.getBytes()), + registration.getId(), request.getPaths(), request.getRequestContentFormat(), + request.getResponseContentFormat(), request.getContext(), Collections.emptyMap()); + + // Add Observation to request context + TransportContext extendedContext = coapRequest.getTransContext() // + .with(LwM2mKeys.LESHAN_OBSERVATION, observation) // + .with(LwM2mKeys.LESHAN_REGISTRATION, registration); + coapRequest = coapRequest.context(extendedContext); + coapRequest = coapRequest.token(token); + } + + @Override + public void visit(CancelCompositeObservationRequest request) { + coapRequest = CoapRequest.fetch(getURI(LwM2mPath.ROOTPATH)).address(getAddress()) + .token(Opaque.of(request.getObservation().getId().getBytes())); + coapRequest.options().setContentFormat((short) request.getRequestContentFormat().getCode()); + coapRequest = coapRequest + .payload(Opaque.of(encoder.encodePaths(request.getPaths(), request.getRequestContentFormat()))); + if (request.getResponseContentFormat() != null) { + coapRequest.options().setAccept(request.getResponseContentFormat().getCode()); + } + coapRequest.options().setObserve(1); + } + + @Override + public void visit(WriteCompositeRequest request) { + coapRequest = CoapRequest.iPatch(getURI(LwM2mPath.ROOTPATH)).address(getAddress()); + coapRequest.options().setContentFormat((short) request.getContentFormat().getCode()); + coapRequest = coapRequest + .payload(Opaque.of(encoder.encodeNodes(request.getNodes(), request.getContentFormat(), model))); + } + + @Override + public void visit(BootstrapWriteRequest request) { + coapRequest = CoapRequest.put(getURI(request.getPath())).address(getAddress()); + ContentFormat format = request.getContentFormat(); + coapRequest.options().setContentFormat((short) format.getCode()); + coapRequest = coapRequest + .payload(Opaque.of(encoder.encode(request.getNode(), format, request.getPath(), model))); + } + + @Override + public void visit(BootstrapReadRequest request) { + coapRequest = CoapRequest.get(getURI(request.getPath())).address(getAddress()); + if (request.getContentFormat() != null) + coapRequest.options().setAccept(request.getContentFormat().getCode()); + } + + @Override + public void visit(BootstrapDiscoverRequest request) { + coapRequest = CoapRequest.get(getURI(request.getPath())).address(getAddress()); + coapRequest.options().setAccept(MediaTypes.CT_APPLICATION_LINK__FORMAT); + } + + @Override + public void visit(BootstrapDeleteRequest request) { + coapRequest = CoapRequest.delete(getURI(request.getPath())).address(getAddress()); + } + + @Override + public void visit(BootstrapFinishRequest request) { + coapRequest = CoapRequest.post("bs").address(getAddress()); + } + + protected InetSocketAddress getAddress() { + if (destination instanceof IpPeer) { + return ((IpPeer) destination).getSocketAddress(); + } else { + throw new IllegalStateException(String.format("Unsupported Peer : %s", destination)); + } + } + + protected String getURI(LwM2mPath path) { + if (path instanceof LwM2mIncompletePath) { + throw new IllegalStateException("Incomplete path can not be used to create request"); + } + + StringBuilder uri = new StringBuilder(); + + // handle root/alternate path + if (rootPath != null && !"/".equals(rootPath)) { + uri.append(rootPath); + } + // add LWM2M request path + uri.append(path.toString()); + return uri.toString(); + } + + public CoapRequest getRequest() { + return coapRequest; + } +} 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 new file mode 100644 index 0000000000..50fbd50510 --- /dev/null +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/request/LwM2mResponseBuilder.java @@ -0,0 +1,542 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.request; + +import java.util.List; +import java.util.Map; + +import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.link.LinkParseException; +import org.eclipse.leshan.core.link.lwm2m.LwM2mLink; +import org.eclipse.leshan.core.link.lwm2m.LwM2mLinkParser; +import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.codec.CodecException; +import org.eclipse.leshan.core.node.codec.LwM2mDecoder; +import org.eclipse.leshan.core.observation.CompositeObservation; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.request.BootstrapDeleteRequest; +import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; +import org.eclipse.leshan.core.request.BootstrapFinishRequest; +import org.eclipse.leshan.core.request.BootstrapReadRequest; +import org.eclipse.leshan.core.request.BootstrapWriteRequest; +import org.eclipse.leshan.core.request.CancelCompositeObservationRequest; +import org.eclipse.leshan.core.request.CancelObservationRequest; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.CreateRequest; +import org.eclipse.leshan.core.request.DeleteRequest; +import org.eclipse.leshan.core.request.DiscoverRequest; +import org.eclipse.leshan.core.request.DownlinkRequestVisitor; +import org.eclipse.leshan.core.request.ExecuteRequest; +import org.eclipse.leshan.core.request.LwM2mRequest; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; +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.WriteAttributesRequest; +import org.eclipse.leshan.core.request.WriteCompositeRequest; +import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.request.exception.InvalidResponseException; +import org.eclipse.leshan.core.response.BootstrapDeleteResponse; +import org.eclipse.leshan.core.response.BootstrapDiscoverResponse; +import org.eclipse.leshan.core.response.BootstrapFinishResponse; +import org.eclipse.leshan.core.response.BootstrapReadResponse; +import org.eclipse.leshan.core.response.BootstrapWriteResponse; +import org.eclipse.leshan.core.response.CancelCompositeObservationResponse; +import org.eclipse.leshan.core.response.CancelObservationResponse; +import org.eclipse.leshan.core.response.CreateResponse; +import org.eclipse.leshan.core.response.DeleteResponse; +import org.eclipse.leshan.core.response.DiscoverResponse; +import org.eclipse.leshan.core.response.ExecuteResponse; +import org.eclipse.leshan.core.response.LwM2mResponse; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; +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.WriteAttributesResponse; +import org.eclipse.leshan.core.response.WriteCompositeResponse; +import org.eclipse.leshan.core.response.WriteResponse; +import org.eclipse.leshan.core.util.Hex; +import org.eclipse.leshan.transport.javacoap.request.ResponseCodeUtil; +import org.eclipse.leshan.transport.javacoap.server.observation.LwM2mKeys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.packet.CoapResponse; +import com.mbed.coap.packet.Code; +import com.mbed.coap.packet.MediaTypes; + +/** + * This class is able to create a {@link LwM2mResponse} from a CoAP {@link CoapResponse}. + *

+ * Call LwM2mResponseBuilder#visit(coapResponse), then get the result using {@link #getResponse()} + * + * @param the type of the response to build. + */ +public class LwM2mResponseBuilder implements DownlinkRequestVisitor { + + private static final Logger LOG = LoggerFactory.getLogger(LwM2mResponseBuilder.class); + + private LwM2mResponse lwM2mresponse; + + private final CoapResponse coapResponse; + private final CoapRequest coapRequest; + + private final String clientEndpoint; + private final LwM2mModel model; + private final LwM2mDecoder decoder; + private final LwM2mLinkParser linkParser; + + public LwM2mResponseBuilder(CoapResponse coapResponse, CoapRequest coapRequest, String clientEndpoint, + LwM2mModel model, LwM2mDecoder decoder, LwM2mLinkParser linkParser) { + this.coapResponse = coapResponse; + this.coapRequest = coapRequest; + + this.clientEndpoint = clientEndpoint; + + this.model = model; + this.decoder = decoder; + this.linkParser = linkParser; + } + + @Override + public void visit(ReadRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new ReadResponse(toLwM2mResponseCode(coapResponse.getCode()), null, + coapResponse.getPayloadString(), coapResponse); + } else if (isResponseCodeContent()) { + // handle success response: + LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint); + lwM2mresponse = new ReadResponse(ResponseCode.CONTENT, content, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(DiscoverRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new DiscoverResponse(toLwM2mResponseCode(coapResponse.getCode()), null, + coapResponse.getPayloadString(), coapResponse); + } else if (isResponseCodeContent()) { + // handle success response: + LwM2mLink[] links; + if (MediaTypes.CT_APPLICATION_LINK__FORMAT != coapResponse.options().getContentFormat()) { + throw new InvalidResponseException("Client [%s] returned unexpected content format [%s] for [%s]", + clientEndpoint, coapResponse.options().getContentFormat(), request); + } else { + try { + // We don't know if root path should be present in discover response. + // See : https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues/534 + String rootpath = null; + links = linkParser.parseLwM2mLinkFromCoreLinkFormat(coapResponse.getPayload().getBytes(), rootpath); + } catch (LinkParseException e) { + throw new InvalidResponseException(e, + "Unable to decode response payload of request [%s] from client [%s]", request, + clientEndpoint); + } + } + lwM2mresponse = new DiscoverResponse(ResponseCode.CONTENT, links, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(WriteRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new WriteResponse(toLwM2mResponseCode(coapResponse.getCode()), + coapResponse.getPayloadString(), coapResponse); + } else if (isResponseCodeChanged()) { + // handle success response: + lwM2mresponse = new WriteResponse(ResponseCode.CHANGED, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(WriteAttributesRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new WriteAttributesResponse(toLwM2mResponseCode(coapResponse.getCode()), + coapResponse.getPayloadString(), coapResponse); + } else if (isResponseCodeChanged()) { + // handle success response: + lwM2mresponse = new WriteAttributesResponse(ResponseCode.CHANGED, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(ExecuteRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new ExecuteResponse(toLwM2mResponseCode(coapResponse.getCode()), + coapResponse.getPayloadString(), coapResponse); + } else if (isResponseCodeChanged()) { + // handle success response: + lwM2mresponse = new ExecuteResponse(ResponseCode.CHANGED, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(CreateRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new CreateResponse(toLwM2mResponseCode(coapResponse.getCode()), null, + coapResponse.getPayloadString(), coapResponse); + } else if (coapResponse.getCode() == Code.C201_CREATED) { + // handle success response: + String locationPath = coapResponse.options().getLocationPath(); + if (locationPath == null || locationPath.equals("/")) { + locationPath = null; + } else if (locationPath.startsWith("/")) { + locationPath = locationPath.substring(1); + } + lwM2mresponse = new CreateResponse(ResponseCode.CREATED, locationPath, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(DeleteRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new DeleteResponse(toLwM2mResponseCode(coapResponse.getCode()), + coapResponse.getPayloadString(), coapResponse); + } else if (coapResponse.getCode() == Code.C202_DELETED) { + // handle success response: + lwM2mresponse = new DeleteResponse(ResponseCode.DELETED, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(ObserveRequest request) { + // TODO implement observe + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new ObserveResponse(toLwM2mResponseCode(coapResponse.getCode()), null, null, null, + coapResponse.getPayloadString(), coapResponse); + } else if (isResponseCodeContent() + // This is for backward compatibility, when the spec say notification used CHANGED code + || isResponseCodeChanged()) { + // handle success response: + LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint); + + if (coapResponse.options().getObserve() != null) { + // Observe relation established + Observation observation = coapRequest.getTransContext().get(LwM2mKeys.LESHAN_OBSERVATION); + if (observation instanceof SingleObservation) { + lwM2mresponse = new ObserveResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null, + (SingleObservation) observation, null, coapResponse); + } else { + throw new IllegalStateException(String.format( + "A Single Observation is expected in coapRequest transport Context, but was %s", + observation == null ? "null" : observation.getClass().getSimpleName())); + } + } else { + // Observe relation NOTestablished + lwM2mresponse = new ObserveResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null, null, + null, coapResponse); + } + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(CancelObservationRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new CancelObservationResponse(toLwM2mResponseCode(coapResponse.getCode()), null, null, null, + coapResponse.getPayloadString(), coapResponse); + } else if (isResponseCodeContent() + // This is for backward compatibility, when the spec say notification used CHANGED code + || isResponseCodeChanged()) { + // handle success response: + LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint); + lwM2mresponse = new CancelObservationResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null, + null, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(ReadCompositeRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new ReadCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), 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); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(ObserveCompositeRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new ObserveCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), null, + coapResponse.getPayloadString(), coapResponse, null); + + } else if (isResponseCodeContent()) { + // handle success response: + Map content = decodeCompositeCoapResponse(request.getPaths(), coapResponse, request, + clientEndpoint); + + if (coapResponse.options().getObserve() != null) { + // Observe relation established + Observation observation = coapRequest.getTransContext().get(LwM2mKeys.LESHAN_OBSERVATION); + if (observation instanceof CompositeObservation) { + lwM2mresponse = new ObserveCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), content, + null, coapResponse, (CompositeObservation) observation); + } else { + throw new IllegalStateException(String.format( + "A Composite Observation is expected in coapRequest transport Context, but was %s", + observation == null ? "null" : observation.getClass().getSimpleName())); + } + } else { + // Observe relation NOTestablished + lwM2mresponse = new ObserveCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null, + coapResponse, null); + } + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(CancelCompositeObservationRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new CancelCompositeObservationResponse(toLwM2mResponseCode(coapResponse.getCode()), null, + coapResponse.getPayloadString(), coapResponse, null); + } else if (isResponseCodeContent() || isResponseCodeChanged()) { + // handle success response: + Map content = decodeCompositeCoapResponse(request.getPaths(), coapResponse, request, + clientEndpoint); + lwM2mresponse = new CancelCompositeObservationResponse(toLwM2mResponseCode(coapResponse.getCode()), content, + null, coapResponse, null); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(WriteCompositeRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new WriteCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), + coapResponse.getPayloadString(), coapResponse); + } else if (isResponseCodeChanged()) { + // handle success response: + lwM2mresponse = new WriteCompositeResponse(ResponseCode.CHANGED, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(BootstrapDiscoverRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new BootstrapDiscoverResponse(toLwM2mResponseCode(coapResponse.getCode()), null, + coapResponse.getPayloadString(), coapResponse); + } else if (isResponseCodeContent()) { + // handle success response: + LwM2mLink[] links; + if (MediaTypes.CT_APPLICATION_LINK__FORMAT != coapResponse.options().getContentFormat()) { + throw new InvalidResponseException("Client [%s] returned unexpected content format [%s] for [%s]", + clientEndpoint, coapResponse.options().getContentFormat(), request); + } else { + try { + links = linkParser.parseLwM2mLinkFromCoreLinkFormat(coapResponse.getPayload().getBytes(), null); + } catch (LinkParseException e) { + throw new InvalidResponseException(e, + "Unable to decode response payload of request [%s] from client [%s]", request, + clientEndpoint); + } + } + lwM2mresponse = new BootstrapDiscoverResponse(ResponseCode.CONTENT, links, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(BootstrapWriteRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new BootstrapWriteResponse(toLwM2mResponseCode(coapResponse.getCode()), + coapResponse.getPayloadString(), coapResponse); + } else if (isResponseCodeChanged()) { + // handle success response: + lwM2mresponse = new BootstrapWriteResponse(ResponseCode.CHANGED, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(BootstrapReadRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new BootstrapReadResponse(toLwM2mResponseCode(coapResponse.getCode()), null, + coapResponse.getPayloadString(), coapResponse); + } else if (isResponseCodeContent()) { + // handle success response: + LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint); + lwM2mresponse = new BootstrapReadResponse(ResponseCode.CONTENT, content, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(BootstrapDeleteRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new BootstrapDeleteResponse(toLwM2mResponseCode(coapResponse.getCode()), + coapResponse.getPayloadString(), coapResponse); + } else if (coapResponse.getCode() == Code.C202_DELETED) { + // handle success response: + lwM2mresponse = new BootstrapDeleteResponse(ResponseCode.DELETED, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + @Override + public void visit(BootstrapFinishRequest request) { + if (coapResponse.getCode().getHttpCode() >= 400) { + // handle error response: + lwM2mresponse = new BootstrapFinishResponse(toLwM2mResponseCode(coapResponse.getCode()), + coapResponse.getPayloadString(), coapResponse); + } else if (isResponseCodeChanged()) { + // handle success response: + lwM2mresponse = new BootstrapFinishResponse(ResponseCode.CHANGED, null, coapResponse); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + + private boolean isResponseCodeContent() { + return coapResponse.getCode() == Code.C205_CONTENT; + } + + private boolean isResponseCodeChanged() { + return coapResponse.getCode() == Code.C204_CHANGED; + } + + public static ResponseCode toLwM2mResponseCode(Code coapResponseCode) { + return ResponseCodeUtil.toLwM2mResponseCode(coapResponseCode); + } + + private LwM2mNode decodeCoapResponse(LwM2mPath path, CoapResponse coapResponse, LwM2mRequest request, + String endpoint) { + + // Get content format + ContentFormat contentFormat = null; + if (coapResponse.options().getContentFormat() != null) { + contentFormat = ContentFormat.fromCode(coapResponse.options().getContentFormat()); + } + + // Decode payload + try { + return decoder.decode(coapResponse.getPayload().getBytes(), contentFormat, path, model); + } catch (CodecException e) { + if (LOG.isDebugEnabled()) { + byte[] payload = coapResponse.getPayload() == null ? new byte[0] : coapResponse.getPayload().getBytes(); + LOG.debug( + String.format("Unable to decode response payload of request [%s] from client [%s] [payload:%s]", + request, endpoint, Hex.encodeHexString(payload))); + } + throw new InvalidResponseException(e, "Unable to decode response payload of request [%s] from client [%s]", + request, endpoint); + } + } + + private Map decodeCompositeCoapResponse(List paths, CoapResponse coapResponse, + LwM2mRequest request, String endpoint) { + // Get content format + ContentFormat contentFormat = null; + if (coapResponse.options().getContentFormat() != null) { + contentFormat = ContentFormat.fromCode(coapResponse.options().getContentFormat()); + } + + // Decode payload + try { + return decoder.decodeNodes(coapResponse.getPayload().getBytes(), contentFormat, paths, model); + } catch (CodecException e) { + if (LOG.isDebugEnabled()) { + byte[] payload = coapResponse.getPayload() == null ? new byte[0] : coapResponse.getPayload().getBytes(); + LOG.debug( + String.format("Unable to decode response payload of request [%s] from client [%s] [payload:%s]", + request, endpoint, Hex.encodeHexString(payload))); + } + throw new InvalidResponseException(e, "Unable to decode response payload of request [%s] from client [%s]", + request, endpoint); + } + } + + @SuppressWarnings("unchecked") + public T getResponse() { + return (T) lwM2mresponse; + } + + private void handleUnexpectedResponseCode(String clientEndpoint, LwM2mRequest request, + CoapResponse coapResponse) { + throw new InvalidResponseException("Client [%s] returned unexpected response code [%s] for [%s]", + clientEndpoint, coapResponse.getCode(), request); + } +} diff --git a/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/resource/RegistrationResource.java b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/resource/RegistrationResource.java new file mode 100644 index 0000000000..d0a7f94be4 --- /dev/null +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/resource/RegistrationResource.java @@ -0,0 +1,254 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.resource; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.leshan.core.link.Link; +import org.eclipse.leshan.core.link.LinkParseException; +import org.eclipse.leshan.core.link.LinkParser; +import org.eclipse.leshan.core.peer.LwM2mPeer; +import org.eclipse.leshan.core.request.BindingMode; +import org.eclipse.leshan.core.request.DeregisterRequest; +import org.eclipse.leshan.core.request.RegisterRequest; +import org.eclipse.leshan.core.request.UpdateRequest; +import org.eclipse.leshan.core.response.DeregisterResponse; +import org.eclipse.leshan.core.response.RegisterResponse; +import org.eclipse.leshan.core.response.SendableResponse; +import org.eclipse.leshan.core.response.UpdateResponse; +import org.eclipse.leshan.server.request.UplinkRequestReceiver; +import org.eclipse.leshan.transport.javacoap.request.ResponseCodeUtil; +import org.eclipse.leshan.transport.javacoap.resource.LwM2mCoapResource; +import org.eclipse.leshan.transport.javacoap.server.endpoint.EndpointUriProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.packet.CoapResponse; +import com.mbed.coap.packet.Code; + +public class RegistrationResource extends LwM2mCoapResource { + + private static final Logger LOG = LoggerFactory.getLogger(RegistrationResource.class); + + private static final String QUERY_PARAM_ENDPOINT = "ep"; + private static final String QUERY_PARAM_BINDING_MODE = "b"; + private static final String QUERY_PARAM_LWM2M_VERSION = "lwm2m"; + private static final String QUERY_PARAM_SMS = "sms"; + private static final String QUERY_PARAM_LIFETIME = "lt"; + private static final String QUERY_PARAM_QUEUEMMODE = "Q"; // since LWM2M 1.1 + + public static final String RESOURCE_NAME = "rd"; + public static final String RESOURCE_URI = "/" + RESOURCE_NAME + "/*"; + + private final UplinkRequestReceiver receiver; + private final LinkParser linkParser; + private final EndpointUriProvider endpointUriProvider; + + public RegistrationResource(UplinkRequestReceiver receiver, LinkParser linkParser, + EndpointUriProvider endpointUriProvider) { + super(RESOURCE_URI); + this.receiver = receiver; + this.linkParser = linkParser; + this.endpointUriProvider = endpointUriProvider; + } + + @Override + public CompletableFuture handlePOST(CoapRequest coapRequest) { + LOG.trace("POST received : {}", coapRequest); + + // validate URI + List uri = getUriPart(coapRequest); + if (uri == null || uri.size() == 0 || !RESOURCE_NAME.equals(uri.get(0))) { + return handleInvalidRequest(coapRequest, "Bad URI"); + } + + // Handle Register or Registration Update + if (uri.size() == 1) { + return handleRegister(coapRequest); + } else if (uri.size() == 2) { + return handleUpdate(coapRequest, uri.get(1)); + } else { + return handleInvalidRequest(coapRequest, "Bad URI"); + } + } + + @Override + public CompletableFuture handleDELETE(CoapRequest coapRequest) { + LOG.trace("DELETE received : {}", coapRequest); + + /// validate URI + List uri = getUriPart(coapRequest); + if (uri != null && uri.size() == 2 && RESOURCE_NAME.equals(uri.get(0))) { + return handleDeregister(coapRequest, uri.get(1)); + } else { + return handleInvalidRequest(coapRequest, "Bad URI"); + } + } + + protected CompletableFuture handleRegister(CoapRequest coapRequest) { + // Get identity + // -------------------------------- + LwM2mPeer sender = getForeignPeerIdentity(coapRequest); + + // Create LwM2m request from CoAP request + // -------------------------------- + // We don't check content media type is APPLICATION LINK FORMAT for now as this is the only format we can expect + String endpoint = null; + Long lifetime = null; + String smsNumber = null; + String lwVersion = null; + EnumSet binding = null; + Boolean queueMode = null; + + // Get object Links + Link[] objectLinks; + try { + objectLinks = linkParser.parseCoreLinkFormat(coapRequest.getPayload().getBytes()); + } catch (LinkParseException e) { + return handleInvalidRequest(coapRequest, e.getMessage() != null ? e.getMessage() : "Invalid Links", e); + } + + Map additionalParams = new HashMap<>(); + + // Get parameters + try { + for (Entry entry : coapRequest.options().getUriQueryMap().entrySet()) { + if (entry.getKey().equals(QUERY_PARAM_ENDPOINT)) { + endpoint = entry.getValue(); + } else if (entry.getKey().equals(QUERY_PARAM_LIFETIME)) { + lifetime = Long.valueOf(entry.getValue()); + } else if (entry.getKey().equals(QUERY_PARAM_SMS)) { + smsNumber = entry.getValue(); + } else if (entry.getKey().equals(QUERY_PARAM_LWM2M_VERSION)) { + lwVersion = entry.getValue(); + } else if (entry.getKey().equals(QUERY_PARAM_BINDING_MODE)) { + binding = BindingMode.parse(entry.getValue()); + } else if (entry.getKey().equals(QUERY_PARAM_QUEUEMMODE)) { + queueMode = true; + } else { + additionalParams.put(entry.getKey(), entry.getValue()); + } + } + } catch (/* NumberFormatException | */ IllegalArgumentException e) { + return handleInvalidRequest(coapRequest, e.getMessage() != null ? e.getMessage() : "Uri Query", e); + } + + // Create request + RegisterRequest registerRequest = new RegisterRequest(endpoint, lifetime, lwVersion, binding, queueMode, + smsNumber, objectLinks, additionalParams, coapRequest); + + // Handle request + // ------------------------------- + final SendableResponse sendableResponse = receiver.requestReceived(sender, null, + registerRequest, endpointUriProvider.getEndpointUri()); + RegisterResponse response = sendableResponse.getResponse(); + + // Create CoAP Response from LwM2m request + // ------------------------------- + // TODO this should be called once request is sent. (No java-coap API for this) + sendableResponse.sent(); + if (response.getCode() == org.eclipse.leshan.core.ResponseCode.CREATED) { + CoapResponse coapResponse = CoapResponse.of(Code.C201_CREATED); + coapResponse.options().setLocationPath(RESOURCE_NAME + "/" + response.getRegistrationID()); + return completedFuture(coapResponse); + } else { + return errorMessage(response.getCode(), response.getErrorMessage()); + } + + } + + protected CompletableFuture handleUpdate(CoapRequest coapRequest, String registrationId) { + // Get identity + LwM2mPeer sender = getForeignPeerIdentity(coapRequest); + + // Create LwM2m request from CoAP request + Long lifetime = null; + String smsNumber = null; + EnumSet binding = null; + Link[] objectLinks = null; + Map additionalParams = new HashMap<>(); + + try { + for (Entry entry : coapRequest.options().getUriQueryMap().entrySet()) { + if (entry.getKey().equals(QUERY_PARAM_LIFETIME)) { + lifetime = Long.valueOf(entry.getValue()); + } else if (entry.getKey().equals(QUERY_PARAM_SMS)) { + smsNumber = entry.getValue(); + } else if (entry.getKey().equals(QUERY_PARAM_BINDING_MODE)) { + binding = BindingMode.parse(entry.getValue()); + } else { + additionalParams.put(entry.getKey(), entry.getValue()); + } + } + } catch (/* NumberFormatException | */ IllegalArgumentException e) { + return handleInvalidRequest(coapRequest, e.getMessage() != null ? e.getMessage() : "Uri Query", e); + } + if (coapRequest.getPayload() != null && coapRequest.getPayload().size() > 0) { + try { + objectLinks = linkParser.parseCoreLinkFormat(coapRequest.getPayload().getBytes()); + } catch (LinkParseException e) { + return handleInvalidRequest(coapRequest, e.getMessage() != null ? e.getMessage() : "Invalid Links", e); + } + } + UpdateRequest updateRequest = new UpdateRequest(registrationId, lifetime, smsNumber, binding, objectLinks, + additionalParams, coapRequest); + + // Handle request + final SendableResponse sendableResponse = receiver.requestReceived(sender, null, updateRequest, + endpointUriProvider.getEndpointUri()); + UpdateResponse updateResponse = sendableResponse.getResponse(); + + // Create CoAP Response from LwM2m request + // TODO this should be called once request is sent. (No java-coap API for this) + sendableResponse.sent(); + if (updateResponse.getCode().isError()) { + return errorMessage(updateResponse.getCode(), updateResponse.getErrorMessage()); + } else { + return completedFuture(CoapResponse.of(ResponseCodeUtil.toCoapResponseCode(updateResponse.getCode()))); + } + + } + + protected CompletableFuture handleDeregister(CoapRequest coapRequest, String registrationId) { + // Get identity + LwM2mPeer sender = getForeignPeerIdentity(coapRequest); + + // Create request + DeregisterRequest deregisterRequest = new DeregisterRequest(registrationId, coapRequest); + + // Handle request + final SendableResponse sendableResponse = receiver.requestReceived(sender, null, + deregisterRequest, endpointUriProvider.getEndpointUri()); + DeregisterResponse deregisterResponse = sendableResponse.getResponse(); + + // Create CoAP Response from LwM2m request + // TODO this should be called once request is sent. (No java-coap API for this) + sendableResponse.sent(); + if (deregisterResponse.getCode().isError()) { + return errorMessage(deregisterResponse.getCode(), deregisterResponse.getErrorMessage()); + } else { + return completedFuture(CoapResponse.of(ResponseCodeUtil.toCoapResponseCode(deregisterResponse.getCode()))); + } + } +} 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 new file mode 100644 index 0000000000..f4f83f62f7 --- /dev/null +++ b/leshan-tl-javacoap-server/src/main/java/org/eclipse/leshan/transport/javacoap/server/resource/SendResource.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright (c) 2023 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: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.resource; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.leshan.core.ResponseCode; +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.peer.LwM2mPeer; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.SendRequest; +import org.eclipse.leshan.core.request.exception.InvalidRequestException; +import org.eclipse.leshan.core.response.SendResponse; +import org.eclipse.leshan.core.response.SendableResponse; +import org.eclipse.leshan.server.profile.ClientProfile; +import org.eclipse.leshan.server.profile.ClientProfileProvider; +import org.eclipse.leshan.server.request.UplinkRequestReceiver; +import org.eclipse.leshan.transport.javacoap.request.ResponseCodeUtil; +import org.eclipse.leshan.transport.javacoap.resource.LwM2mCoapResource; +import org.eclipse.leshan.transport.javacoap.server.endpoint.EndpointUriProvider; + +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.packet.CoapResponse; + +/** + * A CoAP Resource used to handle "Send" request sent by LWM2M devices. + * + * @see SendRequest + */ +public class SendResource extends LwM2mCoapResource { + + public static final String RESOURCE_NAME = "dp"; + public static final String RESOURCE_URI = "/" + RESOURCE_NAME + "/*"; + + private final LwM2mDecoder decoder; + private final UplinkRequestReceiver receiver; + private final ClientProfileProvider profileProvider; + + private final EndpointUriProvider endpointUriProvider; + + public SendResource(UplinkRequestReceiver receiver, LwM2mDecoder decoder, ClientProfileProvider profileProvider, + EndpointUriProvider endpointUriProvider) { + super(RESOURCE_URI); + this.decoder = decoder; + this.receiver = receiver; + this.profileProvider = profileProvider; + this.endpointUriProvider = endpointUriProvider; + } + + @Override + public CompletableFuture handlePOST(CoapRequest coapRequest) { + LwM2mPeer sender = getForeignPeerIdentity(coapRequest); + ClientProfile clientProfile = profileProvider.getProfile(sender.getIdentity()); + + // check we have a registration for this identity + if (clientProfile == null) { + return errorMessage(ResponseCode.BAD_REQUEST, "no registration found"); + } + + try { + // Decode payload + byte[] payload = coapRequest.getPayload().getBytes(); + ContentFormat contentFormat = ContentFormat.fromCode(coapRequest.options().getContentFormat()); + if (!decoder.isSupported(contentFormat)) { + // TODO receiver call should maybe called after we send the response... + // (not sure there is a way to do that) + receiver.onError(sender, clientProfile, + new InvalidRequestException("Unsupported content format [%s] in [%s] from [%s]", contentFormat, + coapRequest, sender), + SendRequest.class, endpointUriProvider.getEndpointUri()); + return errorMessage(ResponseCode.BAD_REQUEST, "Unsupported content format"); + } + + TimestampedLwM2mNodes data = decoder.decodeTimestampedNodes(payload, contentFormat, + clientProfile.getModel()); + + // Handle "send op request + SendRequest sendRequest = new SendRequest(contentFormat, data, coapRequest); + SendableResponse sendableResponse = receiver.requestReceived(sender, clientProfile, + sendRequest, endpointUriProvider.getEndpointUri()); + SendResponse response = sendableResponse.getResponse(); + + // send response + // TODO this should be called once request is sent. (No java-coap API for this) + sendableResponse.sent(); + if (response.isSuccess()) { + return completedFuture(CoapResponse.of(ResponseCodeUtil.toCoapResponseCode(response.getCode()))); + } else { + return errorMessage(response.getCode(), response.getErrorMessage()); + } + } catch (CodecException e) { + // TODO receiver call should maybe called after we send the response... + // (not sure there is a way to do that) + receiver.onError(sender, clientProfile, + new InvalidRequestException(e, "Invalid payload in [%s] from [%s]", coapRequest, sender), + SendRequest.class, endpointUriProvider.getEndpointUri()); + return errorMessage(ResponseCode.BAD_REQUEST, "Invalid Payload"); + } catch (RuntimeException e) { + receiver.onError(sender, clientProfile, e, SendRequest.class, endpointUriProvider.getEndpointUri()); + throw e; + } + } +} diff --git a/pom.xml b/pom.xml index c68bc195ad..fca3ec0cfb 100644 --- a/pom.xml +++ b/pom.xml @@ -91,6 +91,9 @@ Contributors: leshan-server-redis leshan-client-core leshan-client-cf + + leshan-tl-javacoap-core + leshan-tl-javacoap-server leshan-integration-tests @@ -174,6 +177,18 @@ Contributors: leshan-server-redis ${project.version} + + + ${project.groupId} + leshan-tl-javacoap-core + ${project.version} + + + ${project.groupId} + leshan-tl-javacoap-server + ${project.version} + + ${project.groupId} leshan-core-demo @@ -186,11 +201,29 @@ Contributors: + org.slf4j slf4j-api ${slf4j.api.version} + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.upokecenter + cbor + 4.5.2 + + + + redis.clients + jedis + 4.4.3 + + org.eclipse.californium californium-core @@ -206,20 +239,11 @@ Contributors: scandium ${californium.version} + - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - - com.upokecenter - cbor - 4.5.2 - - - redis.clients - jedis - 4.4.3 + io.github.open-coap + coap-core + 6.18.0 From 732203255485d19aeac57a2046d9e5c79428d016 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Fri, 14 Apr 2023 17:34:14 +0200 Subject: [PATCH 2/3] Test Server CoAP endpoint based on java-coap in integration tests. --- leshan-integration-tests/pom.xml | 4 + .../leshan/integration/tests/DeleteTest.java | 3 +- .../integration/tests/DiscoverTest.java | 3 +- .../leshan/integration/tests/ExecuteTest.java | 3 +- .../integration/tests/QueueModeTest.java | 3 +- .../integration/tests/RegistrationTest.java | 61 +------------- .../tests/create/CreateFailedTest.java | 3 +- .../integration/tests/create/CreateTest.java | 3 +- .../tests/lockstep/LockStepTest.java | 81 ++++++++++++++++++- .../tests/observe/DynamicIPObserveTest.java | 3 +- .../tests/observe/ObserveCompositeTest.java | 3 +- .../tests/observe/ObserveServerOnlyTest.java | 3 +- .../tests/observe/ObserveTest.java | 3 +- .../tests/observe/ObserveTimeStampTest.java | 3 +- .../tests/read/ReadCompositeTest.java | 7 +- .../tests/read/ReadFailedTest.java | 3 +- .../tests/read/ReadMultiValueTest.java | 3 +- .../tests/read/ReadOpaqueValueTest.java | 3 +- .../tests/read/ReadSingleValueTest.java | 3 +- .../tests/send/DynamicIPSendTest.java | 3 +- .../tests/send/LockStepSendTest.java | 27 ++++++- .../integration/tests/send/SendTest.java | 3 +- .../tests/send/SendTimestampedTest.java | 3 +- .../tests/util/LeshanTestServerBuilder.java | 11 +++ .../AbstractLwM2mResponseAssert.java | 11 +++ .../tests/write/WriteCompositeTest.java | 8 +- .../tests/write/WriteFailedTest.java | 3 +- .../tests/write/WriteMultiValueTest.java | 3 +- .../tests/write/WriteOpaqueValueTest.java | 3 +- .../tests/write/WriteSingleValueTest.java | 3 +- 30 files changed, 184 insertions(+), 92 deletions(-) diff --git a/leshan-integration-tests/pom.xml b/leshan-integration-tests/pom.xml index 0ade6c6b5f..f964f51f4a 100644 --- a/leshan-integration-tests/pom.xml +++ b/leshan-integration-tests/pom.xml @@ -38,6 +38,10 @@ Contributors: org.eclipse.leshan leshan-server-cf + + org.eclipse.leshan + leshan-tl-javacoap-server + org.eclipse.leshan leshan-server-redis diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/DeleteTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/DeleteTest.java index 881534ec2a..8b6798ce93 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/DeleteTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/DeleteTest.java @@ -62,7 +62,8 @@ public class DeleteTest { static Stream transports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAP, "Californium", "Californium")); + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap")); } /*---------------------------------/ diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/DiscoverTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/DiscoverTest.java index 8f8935c226..218d1aaca4 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/DiscoverTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/DiscoverTest.java @@ -56,7 +56,8 @@ public class DiscoverTest { static Stream transports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAP, "Californium", "Californium")); + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap")); } /*---------------------------------/ diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ExecuteTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ExecuteTest.java index 400d675450..b227028edd 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ExecuteTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ExecuteTest.java @@ -59,7 +59,8 @@ public class ExecuteTest { static Stream transports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAP, "Californium", "Californium")); + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap")); } /*---------------------------------/ diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeTest.java index 64e273921c..cbac6b84db 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeTest.java @@ -58,7 +58,8 @@ public class QueueModeTest { static Stream transports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAP, "Californium", "Californium")); + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap")); } /*---------------------------------/ diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RegistrationTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RegistrationTest.java index cea978a8fb..8be5f0b2bd 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RegistrationTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RegistrationTest.java @@ -19,12 +19,9 @@ package org.eclipse.leshan.integration.tests; 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.assertion.Assertions.assertArg; import static org.eclipse.leshan.integration.tests.util.assertion.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrowsExactly; -import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -41,7 +38,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -56,16 +52,10 @@ import org.eclipse.californium.elements.config.UdpConfig; import org.eclipse.leshan.core.endpoint.Protocol; import org.eclipse.leshan.core.link.LinkParseException; -import org.eclipse.leshan.core.node.LwM2mPath; -import org.eclipse.leshan.core.observation.Observation; -import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.ContentFormat; -import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.core.request.ReadRequest; import org.eclipse.leshan.core.request.exception.RequestCanceledException; -import org.eclipse.leshan.core.request.exception.SendFailedException; import org.eclipse.leshan.core.response.ErrorCallback; -import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.core.response.ResponseCallback; import org.eclipse.leshan.integration.tests.util.LeshanTestClient; @@ -98,7 +88,8 @@ public class RegistrationTest { static Stream transports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAP, "Californium", "Californium")); + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap")); } /*---------------------------------/ @@ -278,54 +269,6 @@ public void register_update_reregister(Protocol protocol, String clientEndpointP assertThat(client).isRegisteredAt(server); } - @TestAllTransportLayer - public void register_observe_deregister_observe(Protocol protocol, String clientEndpointProvider, - String serverEndpointProvider) throws NonUniqueSecurityInfoException, InterruptedException { - // TODO java-coap does not raise expected SendFailedException at the end of this tests - // But not sure what should be the right behavior. - // Waiting for https://github.com/open-coap/java-coap/issues/36 before to move forward on this. - assumeTrue(serverEndpointProvider.equals("Californium")); - - // Check client is not registered - client = givenClient.build(); - assertThat(client).isNotRegisteredAt(server); - - // Start it and wait for registration - client.start(); - server.waitForNewRegistrationOf(client); - client.waitForRegistrationTo(server); - - // Check client is well registered - assertThat(client).isRegisteredAt(server); - Registration registration = server.getRegistrationFor(client); - - // observe device timezone - ObserveResponse observeResponse = server.send(registration, new ObserveRequest(3, 0)); - assertThat(observeResponse) // - .hasCode(CONTENT) // - .hasValidUnderlyingResponseFor(serverEndpointProvider); - - // check observation registry is not null - Set observations = server.getObservationService().getObservations(registration); - assertThat(observations) // - .hasSize(1) // - .first().isInstanceOfSatisfying(SingleObservation.class, obs -> { - assertThat(obs.getRegistrationId()).isEqualTo(registration.getId()); - assertThat(obs.getPath()).isEqualTo(new LwM2mPath(3, 0)); - }); - - // Check de-registration - client.stop(true); - server.waitForDeregistrationOf(registration, observations); - assertThat(client).isNotRegisteredAt(server); - client.waitForDeregistrationTo(server); - observations = server.getObservationService().getObservations(registration); - assertThat(observations).isEmpty(); - - // try to send a new observation - assertThrowsExactly(SendFailedException.class, () -> server.send(registration, new ObserveRequest(3, 0), 50)); - } - @TestAllTransportLayer public void register_with_additional_attributes(Protocol protocol, String clientEndpointProvider, String serverEndpointProvider) throws InterruptedException, LinkParseException { diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateFailedTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateFailedTest.java index d1dcfdab06..2307a6b939 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateFailedTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateFailedTest.java @@ -55,7 +55,8 @@ public class CreateFailedTest { static Stream transports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAP, "Californium", "Californium")); + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap")); } /*---------------------------------/ diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateTest.java index e18fccb30b..d76f666d25 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateTest.java @@ -72,7 +72,8 @@ static Stream transports() { Object[][] transports = new Object[][] { // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - { Protocol.COAP, "Californium", "Californium" } }; + { Protocol.COAP, "Californium", "Californium" }, // + { Protocol.COAP, "Californium", "java-coap" } }; Object[] contentFormats = new Object[] { // ContentFormat.TLV, // 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 1764a10d3c..e40112ed0d 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 @@ -27,10 +27,15 @@ import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.time.Duration; import java.util.EnumSet; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -46,12 +51,18 @@ import org.eclipse.leshan.core.endpoint.Protocol; import org.eclipse.leshan.core.link.LinkParser; import org.eclipse.leshan.core.link.lwm2m.DefaultLwM2mLinkParser; +import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.request.BindingMode; +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.ReadRequest; import org.eclipse.leshan.core.request.RegisterRequest; import org.eclipse.leshan.core.request.UpdateRequest; +import org.eclipse.leshan.core.request.exception.SendFailedException; import org.eclipse.leshan.core.request.exception.TimeoutException; import org.eclipse.leshan.core.response.ErrorCallback; +import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.core.response.ResponseCallback; import org.eclipse.leshan.integration.tests.util.LeshanTestServer; @@ -59,13 +70,18 @@ import org.eclipse.leshan.integration.tests.util.junit5.extensions.BeforeEachParameterizedResolver; import org.eclipse.leshan.server.californium.endpoint.ServerProtocolProvider; import org.eclipse.leshan.server.californium.endpoint.coap.CoapServerProtocolProvider; +import org.eclipse.leshan.server.endpoint.LwM2mServerEndpointsProvider; import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.transport.javacoap.server.endpoint.JavaCoapServerEndpointsProvider; 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.MethodSource; +import com.mbed.coap.server.CoapServerBuilder; +import com.mbed.coap.transmission.RetransmissionBackOff; + @ExtendWith(BeforeEachParameterizedResolver.class) public class LockStepTest { public static final LinkParser linkParser = new DefaultLwM2mLinkParser(); @@ -82,7 +98,8 @@ public class LockStepTest { static Stream transports() { return Stream.of(// // Server Endpoint Provider - arguments("Californium")); + arguments("Californium"), // + arguments("java-coap")); } /*---------------------------------/ @@ -97,7 +114,7 @@ protected ServerProtocolProvider getCaliforniumProtocolProvider(Protocol protoco public void applyDefaultValue(Configuration configuration) { super.applyDefaultValue(configuration); // configure retransmission, with this configuration a request without ACK should timeout in - // ~200*5ms + // ~200*5ms = 1s configuration.set(CoapConfig.ACK_TIMEOUT, 200, TimeUnit.MILLISECONDS) // .set(CoapConfig.ACK_INIT_RANDOM, 1f) // .set(CoapConfig.ACK_TIMEOUT_SCALE, 1f) // @@ -105,6 +122,20 @@ public void applyDefaultValue(Configuration configuration) { } }; } + + @Override + protected LwM2mServerEndpointsProvider getJavaCoapProtocolProvider(Protocol protocol) { + return new JavaCoapServerEndpointsProvider(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)) { + @Override + protected CoapServerBuilder createCoapServer() { + return super.createCoapServer() + // configure retransmission, with this configuration a request without ACK should + // timeout in + // 140 + 2*140 + 4*140 = ~1s + .retransmission(RetransmissionBackOff.ofExponential(Duration.ofMillis(140), 2, 1)); + } + }; + } }; } @@ -329,4 +360,50 @@ public void async_send_with_acknowleged_request_without_response(String givenSer })); verify(responseCallback, never()).onResponse(any()); } + + @TestAllTransportLayer + public void register_deregister_observe(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()); + + // deregister client + token = client.sendLwM2mRequest(new DeregisterRequest("rd/" + registration.getId())); + client.expectResponse().token(token).go(); + server.waitForDeregistrationOf(registration); + + // send read + @SuppressWarnings("unchecked") + ResponseCallback responseCallback = mock(ResponseCallback.class); + ErrorCallback errorCallback = mock(ErrorCallback.class); + + server.send(registration, new ObserveRequest(ContentFormat.TEXT_CODE, 3, 0), 500l, responseCallback, + errorCallback); + + if (givenServerEndpointProvider.equals("Californium")) { + // with californium endpoint provider "SendFailedException" is raised because, + // we try to add the relation in store before to send the request and registration doesn't exist anymorev + verify(errorCallback, timeout(200).times(1)) // + .onError(assertArg(e -> { + assertThat(e).isInstanceOf(SendFailedException.class); + })); + } else { + // with java-coap it failed transparently at response reception. + // TODO I don't know if this is the right behavior. + client.expectRequest().storeMID("R").storeToken("T").go(); + client.sendResponse(Type.ACK, ResponseCode.CONTENT).payload("aaa").observe(2).loadMID("R").loadToken("T") + .go(); + } + + // ensure we don't get answer and there is no observation in store. + verifyNoMoreInteractions(responseCallback, errorCallback); + assertThat(server.getRegistrationService().getAllRegistrations().hasNext() == false); + Set observations = server.getObservationService().getObservations(registration); + assertThat(observations).isEmpty(); + } } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/DynamicIPObserveTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/DynamicIPObserveTest.java index b3f8f266ad..8e96e06303 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/DynamicIPObserveTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/DynamicIPObserveTest.java @@ -78,7 +78,8 @@ public class DynamicIPObserveTest { static Stream noTlsTransports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAP, "Californium", "Californium")); + arguments(Protocol.COAP, "Californium", "Californium"), + arguments(Protocol.COAP, "Californium", "java-coap")); } @ParameterizedTest(name = "{0} - Client using {1} - Server using {2}") diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTest.java index 405120b9bf..d7b3a71715 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTest.java @@ -74,7 +74,8 @@ public class ObserveCompositeTest { static Stream transports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAP, "Californium", "Californium")); + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap")); } /*---------------------------------/ diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveServerOnlyTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveServerOnlyTest.java index f8da9d2eca..73c23a5b59 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveServerOnlyTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveServerOnlyTest.java @@ -68,7 +68,8 @@ public class ObserveServerOnlyTest { static Stream transports() { return Stream.of(// // Server Endpoint Provider - arguments("Californium")); + arguments("Californium"), // + arguments("java-coap")); } /*---------------------------------/ diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java index 3cc297c2a1..501d083689 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java @@ -76,7 +76,8 @@ public class ObserveTest { static Stream transports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAP, "Californium", "Californium")); + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap")); } /*---------------------------------/ 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 db688e0598..424728c3f1 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 @@ -70,7 +70,8 @@ static Stream transports() { Object[][] transports = new Object[][] { // Server Endpoint Provider - { "Californium" } }; + { "Californium" }, // + { "java-coap" } }; Object[] contentFormats = new Object[] { // ContentFormat.JSON, // diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadCompositeTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadCompositeTest.java index 04b95efaea..788ac02ac0 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadCompositeTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadCompositeTest.java @@ -60,9 +60,10 @@ public class ReadCompositeTest { static Stream transports() { - Object[][] transports = new Object[][] - // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - { { Protocol.COAP, "Californium", "Californium" } }; + Object[][] transports = new Object[][] { + // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider + { Protocol.COAP, "Californium", "Californium" }, // + { Protocol.COAP, "Californium", "java-coap" } }; Object[][] contentFormats = new Object[][] { // // {request content format, response content format} diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadFailedTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadFailedTest.java index 812cd77bc5..c86a663645 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadFailedTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadFailedTest.java @@ -54,7 +54,8 @@ public class ReadFailedTest { static Stream transports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAP, "Californium", "Californium")); + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap")); } /*---------------------------------/ diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java index a932fdbe2d..f52193a011 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java @@ -61,7 +61,8 @@ static Stream transports() { Object[][] transports = new Object[][] { // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - { Protocol.COAP, "Californium", "Californium" } }; + { Protocol.COAP, "Californium", "Californium" }, // + { Protocol.COAP, "Californium", "java-coap" } }; Object[] contentFormats = new Object[] { // ContentFormat.TLV, // diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadOpaqueValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadOpaqueValueTest.java index 2226d4d35c..d45e35d847 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadOpaqueValueTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadOpaqueValueTest.java @@ -62,7 +62,8 @@ static Stream transports() { Object[][] transports = new Object[][] { // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - { Protocol.COAP, "Californium", "Californium" } }; + { Protocol.COAP, "Californium", "Californium" }, // + { Protocol.COAP, "Californium", "java-coap" } }; Object[] contentFormats = new Object[] { // ContentFormat.OPAQUE, // diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadSingleValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadSingleValueTest.java index 9b57fea411..0717be844f 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadSingleValueTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadSingleValueTest.java @@ -62,7 +62,8 @@ static Stream transports() { Object[][] transports = new Object[][] { // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - { Protocol.COAP, "Californium", "Californium" } }; + { Protocol.COAP, "Californium", "Californium" }, // + { Protocol.COAP, "Californium", "java-coap" } }; Object[] contentFormats = new Object[] { // ContentFormat.TEXT, // diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/DynamicIPSendTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/DynamicIPSendTest.java index 5f86df90b5..027a79afa7 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/DynamicIPSendTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/DynamicIPSendTest.java @@ -77,7 +77,8 @@ public class DynamicIPSendTest { static Stream noTlsTransports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAP, "Californium", "Californium")); + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap")); } @ParameterizedTest(name = "{0} - Client using {1} - Server using {2}") diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/LockStepSendTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/LockStepSendTest.java index d87eff7933..31b465773c 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/LockStepSendTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/LockStepSendTest.java @@ -20,6 +20,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.time.Duration; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; @@ -47,12 +50,17 @@ import org.eclipse.leshan.integration.tests.util.junit5.extensions.BeforeEachParameterizedResolver; import org.eclipse.leshan.server.californium.endpoint.ServerProtocolProvider; import org.eclipse.leshan.server.californium.endpoint.coap.CoapServerProtocolProvider; +import org.eclipse.leshan.server.endpoint.LwM2mServerEndpointsProvider; +import org.eclipse.leshan.transport.javacoap.server.endpoint.JavaCoapServerEndpointsProvider; 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.MethodSource; +import com.mbed.coap.server.CoapServerBuilder; +import com.mbed.coap.transmission.RetransmissionBackOff; + @ExtendWith(BeforeEachParameterizedResolver.class) public class LockStepSendTest { public static final LinkParser linkParser = new DefaultLwM2mLinkParser(); @@ -69,7 +77,8 @@ public class LockStepSendTest { static Stream transports() { return Stream.of(// // ProtocolUsed - Server Endpoint Provider - arguments("Californium")); + arguments("Californium"), // + arguments("java-coap")); } /*---------------------------------/ @@ -84,7 +93,7 @@ protected ServerProtocolProvider getCaliforniumProtocolProvider(Protocol protoco public void applyDefaultValue(Configuration configuration) { super.applyDefaultValue(configuration); // configure retransmission, with this configuration a request without ACK should timeout in - // ~200*5ms + // ~200*5ms = 1s configuration.set(CoapConfig.ACK_TIMEOUT, 200, TimeUnit.MILLISECONDS) // .set(CoapConfig.ACK_INIT_RANDOM, 1f) // .set(CoapConfig.ACK_TIMEOUT_SCALE, 1f) // @@ -92,6 +101,20 @@ public void applyDefaultValue(Configuration configuration) { } }; } + + @Override + protected LwM2mServerEndpointsProvider getJavaCoapProtocolProvider(Protocol protocol) { + return new JavaCoapServerEndpointsProvider(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)) { + @Override + protected CoapServerBuilder createCoapServer() { + return super.createCoapServer() + // configure retransmission, with this configuration a request without ACK should + // timeout in + // 140 + 2*140 + 4*140 = ~1s + .retransmission(RetransmissionBackOff.ofExponential(Duration.ofMillis(140), 2, 1)); + } + }; + } }; } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTest.java index b7c28fb66a..dd9d601bfd 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTest.java @@ -70,7 +70,8 @@ static Stream transports() { Object[][] transports = new Object[][] { // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - { Protocol.COAP, "Californium", "Californium" } }; + { Protocol.COAP, "Californium", "Californium" }, // + { Protocol.COAP, "Californium", "java-coap" } }; Object[] contentFormats = new Object[] { // ContentFormat.SENML_JSON, // diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTimestampedTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTimestampedTest.java index f5d1c23d52..3d3de6599f 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTimestampedTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTimestampedTest.java @@ -66,7 +66,8 @@ static Stream transports() { Object[][] transports = new Object[][] { // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - { Protocol.COAP, "Californium", "Californium" } }; + { Protocol.COAP, "Californium", "Californium" }, // + { Protocol.COAP, "Californium", "java-coap" } }; Object[] contentFormats = new Object[] { // ContentFormat.SENML_JSON, // diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServerBuilder.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServerBuilder.java index c3206628f8..7e477daa78 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServerBuilder.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServerBuilder.java @@ -52,6 +52,7 @@ import org.eclipse.leshan.server.security.EditableSecurityStore; import org.eclipse.leshan.server.security.SecurityStore; import org.eclipse.leshan.server.security.ServerSecurityInfo; +import org.eclipse.leshan.transport.javacoap.server.endpoint.JavaCoapServerEndpointsProvider; public class LeshanTestServerBuilder extends LeshanServerBuilder { @@ -99,6 +100,9 @@ protected LeshanTestServer createServer(LwM2mServerEndpointsProvider endpointsPr builder.addEndpoint(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), protocolToUse); endpointsProvider = builder.build(); break; + case "java-coap": + endpointsProvider = getJavaCoapProtocolProvider(protocolToUse); + break; default: throw new IllegalStateException( String.format("Unknown endpoint provider : [%s]", endpointProviderName)); @@ -206,4 +210,11 @@ public CaliforniumServerEndpointFactory createDefaultEndpointFactory(URI uri) { throw new IllegalStateException( String.format("No Californium Protocol Provider supporting OSCORE for protocol %s", protocol)); } + + protected LwM2mServerEndpointsProvider getJavaCoapProtocolProvider(Protocol protocol) { + if (protocolToUse.equals(Protocol.COAP)) { + return new JavaCoapServerEndpointsProvider(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); + } + throw new IllegalStateException(String.format("No Californium Protocol Provider for protocol %s", protocol)); + } } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/assertion/AbstractLwM2mResponseAssert.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/assertion/AbstractLwM2mResponseAssert.java index 2147e31a6b..6aabe75c29 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/assertion/AbstractLwM2mResponseAssert.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/assertion/AbstractLwM2mResponseAssert.java @@ -22,6 +22,8 @@ import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.response.LwM2mResponse; +import com.mbed.coap.packet.CoapResponse; + public abstract class AbstractLwM2mResponseAssert, ACTUAL extends LwM2mResponse> extends AbstractObjectAssert { @@ -54,6 +56,9 @@ public SELF hasValidUnderlyingResponseFor(String givenEndpointProvider) { case "Californium-OSCORE": assertThatUnderlyingResponse.isExactlyInstanceOf(Response.class); break; + case "java-coap": + assertThatUnderlyingResponse.isExactlyInstanceOf(CoapResponse.class); + break; default: throw new IllegalStateException(String.format("Unsupported endpoint provider : %s", givenEndpointProvider)); } @@ -72,6 +77,12 @@ public SELF hasContentFormat(ContentFormat format, String givenEndpointProvider) .isEqualTo(format.getCode()); }); break; + case "java-coap": + assertThatUnderlyingResponse.isInstanceOfSatisfying(CoapResponse.class, r -> { + Assertions.assertThat(r.options().getContentFormat()).as("Content Format")// + .isNotNull().isEqualTo((short) format.getCode()); + }); + break; default: throw new IllegalStateException(String.format("Unsupported endpoint provider : %s", givenEndpointProvider)); } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteCompositeTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteCompositeTest.java index ede04a6e14..56b182b5ff 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteCompositeTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteCompositeTest.java @@ -72,10 +72,10 @@ public class WriteCompositeTest { } static Stream transports() { - - Object[][] transports = new Object[][] - // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - { { Protocol.COAP, "Californium", "Californium" } }; + Object[][] transports = new Object[][] { + // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider + { Protocol.COAP, "Californium", "Californium" }, // + { Protocol.COAP, "Californium", "java-coap" } }; Object[] contentFormats = new Object[] { // ContentFormat.SENML_JSON, // diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteFailedTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteFailedTest.java index 58ef6662ae..fe1c7433f4 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteFailedTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteFailedTest.java @@ -58,7 +58,8 @@ public class WriteFailedTest { static Stream transports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAP, "Californium", "Californium")); + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap")); } /*---------------------------------/ diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java index bdbd570c78..b202c28f8a 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java @@ -72,7 +72,8 @@ static Stream transports() { Object[][] transports = new Object[][] { // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - { Protocol.COAP, "Californium", "Californium" } }; + { Protocol.COAP, "Californium", "Californium" }, // + { Protocol.COAP, "Californium", "java-coap" } }; Object[] contentFormats = new Object[] { // ContentFormat.TLV, // diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java index e646b03c99..6d1f1c94a6 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java @@ -65,7 +65,8 @@ static Stream transports() { Object[][] transports = new Object[][] { // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - { Protocol.COAP, "Californium", "Californium" } }; + { Protocol.COAP, "Californium", "Californium" }, // + { Protocol.COAP, "Californium", "java-coap" } }; Object[] contentFormats = new Object[] { // ContentFormat.OPAQUE, // diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteSingleValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteSingleValueTest.java index 3b4ec2dc15..50c990c81d 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteSingleValueTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteSingleValueTest.java @@ -78,7 +78,8 @@ static Stream transports() { Object[][] transports = new Object[][] { // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - { Protocol.COAP, "Californium", "Californium" } }; + { Protocol.COAP, "Californium", "Californium" }, // + { Protocol.COAP, "Californium", "java-coap" } }; Object[] contentFormats = new Object[] { // ContentFormat.TEXT, // From edc4edf06a983fa9d8471cf3711a397bb600d7c6 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Mon, 16 Oct 2023 11:11:17 +0200 Subject: [PATCH 3/3] Add CoAP endpoint based on java-coap to leshan-server-demo --- leshan-server-demo/pom.xml | 4 ++++ .../leshan/server/demo/LeshanServerDemo.java | 9 ++++++++- .../server/demo/cli/LeshanServerDemoCLI.java | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/leshan-server-demo/pom.xml b/leshan-server-demo/pom.xml index 41d9f321f1..84e9af5c94 100644 --- a/leshan-server-demo/pom.xml +++ b/leshan-server-demo/pom.xml @@ -49,6 +49,10 @@ Contributors: org.eclipse.leshan leshan-server-redis + + org.eclipse.leshan + leshan-tl-javacoap-server + org.eclipse.californium californium-core diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java index e9f194df38..778f684f69 100644 --- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java +++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java @@ -61,6 +61,7 @@ import org.eclipse.leshan.server.redis.RedisSecurityStore; import org.eclipse.leshan.server.security.EditableSecurityStore; import org.eclipse.leshan.server.security.FileSecurityStore; +import org.eclipse.leshan.transport.javacoap.server.endpoint.JavaCoapServerEndpointsProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -249,8 +250,14 @@ public static LeshanServer createLeshanServer(LeshanServerDemoCLI cli) throws Ex : new InetSocketAddress(cli.main.secureLocalAddress, coapsPort); endpointsBuilder.addEndpoint(coapsAddr, Protocol.COAPS); + // Create CoAP endpoint based on java-coap + int jcoapPort = cli.main.jlocalPort; + InetSocketAddress jcoapAddr = cli.main.secureLocalAddress == null ? new InetSocketAddress(jcoapPort) + : new InetSocketAddress(cli.main.jlocalAddress, jcoapPort); + JavaCoapServerEndpointsProvider javacoapEndpointsProvider = new JavaCoapServerEndpointsProvider(jcoapAddr); + // Create LWM2M server - builder.setEndpointsProviders(endpointsBuilder.build()); + builder.setEndpointsProviders(endpointsBuilder.build(), javacoapEndpointsProvider); return builder.build(); } diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/cli/LeshanServerDemoCLI.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/cli/LeshanServerDemoCLI.java index a770a0b4e0..eca3391330 100644 --- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/cli/LeshanServerDemoCLI.java +++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/cli/LeshanServerDemoCLI.java @@ -19,6 +19,7 @@ import org.eclipse.leshan.core.demo.cli.StandardHelpOptions; import org.eclipse.leshan.core.demo.cli.VersionProvider; +import org.eclipse.leshan.core.demo.cli.converters.PortConverter; import org.eclipse.leshan.server.core.demo.cli.DtlsSection; import org.eclipse.leshan.server.core.demo.cli.GeneralSection; import org.eclipse.leshan.server.core.demo.cli.IdentitySection; @@ -55,6 +56,19 @@ public class LeshanServerDemoCLI implements Runnable { public ServerGeneralSection main = new ServerGeneralSection(); public static class ServerGeneralSection extends GeneralSection { + @Option(names = { "-jh", "--java-coap-host" }, + description = { // + "Set the local CoAP address of endpoint based on java-coap library.", // + "Default: any local address." }) + public String jlocalAddress; + + @Option(names = { "-jp", "--java-coap-port" }, + description = { // + "Set the local CoAP port of endpoint based on java-coap library.", // + "Default: ${DEFAULT-VALUE}" }, + converter = PortConverter.class) + public Integer jlocalPort = 5685; + @Option(names = { "-r", "--redis" }, description = { // "Use redis to store registration and securityInfo.", //