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}