From d8ef31ad164943806ff3180858277c45edb87ae2 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Tue, 19 Sep 2023 15:56:44 +0200 Subject: [PATCH] GH-534:Add support of pmin,pmax,lt,gt,st for simple observe to client. Co-authored-by: Magdalena Kundera --- .../CaliforniumClientEndpointsProvider.java | 5 +- .../endpoint/ClientCoapMessageTranslator.java | 14 +- .../californium/object/ObjectResource.java | 122 ++- .../eclipse/leshan/client/LeshanClient.java | 30 +- ...faultCompositeClientEndpointsProvider.java | 5 +- .../LwM2mClientEndpointsProvider.java | 4 +- .../DefaultNotificationStrategy.java | 134 ++++ .../notification/NotificationDataStore.java | 228 ++++++ .../notification/NotificationManager.java | 272 +++++++ .../notification/NotificationStrategy.java | 84 +++ .../checker/CriteriaBasedOnValueChecker.java | 37 + .../notification/checker/FloatChecker.java | 64 ++ .../notification/checker/IntegerChecker.java | 64 ++ .../checker/UnsignedIntegerChecker.java | 76 ++ .../client/resource/BaseObjectEnabler.java | 79 +- .../client/resource/LwM2mObjectEnabler.java | 3 + .../client/resource/NotificationSender.java | 25 + .../leshan/client/util/LinkFormatHelper.java | 55 +- .../link/lwm2m/DefaultLwM2mLinkParser.java | 5 +- .../InvalidAttributesException.java | 40 + .../lwm2m/attributes/LwM2mAttributeModel.java | 4 +- .../lwm2m/attributes/LwM2mAttributes.java | 38 +- .../attributes/MixedLwM2mAttributeSet.java | 106 ++- .../attributes/NotificationAttributeTree.java | 92 +++ .../core/request/WriteAttributesRequest.java | 23 +- .../link/attributes/AttributeSetTest.java | 12 +- .../core/link/attributes/AttributeTest.java | 47 ++ .../request/WriteAttributesRequestTest.java | 66 +- .../WriteAttributeBootstrapTest.java | 204 +++++ .../WriteAttributeDiscoverTest.java | 427 +++++++++++ .../attributes/WriteAttributeFailedTest.java | 161 ++++ .../WriteAttributeHouseKeepingTest.java | 382 ++++++++++ .../attributes/WriteAttributeObserveTest.java | 711 ++++++++++++++++++ .../tests/util/LeshanTestClient.java | 18 + .../tests/util/LeshanTestServer.java | 9 + .../AbstractLwM2mResponseAssert.java | 10 + .../assertion/LeshanTestClientAssert.java | 60 ++ .../JavaCoapClientEndpointsProvider.java | 20 +- .../client/observe/HashMapObserversStore.java | 46 +- .../javacoap/client/observe/LwM2mKeys.java | 1 + .../client/observe/ObserversListener.java | 25 + .../client/observe/ObserversManager.java | 27 +- .../client/observe/ObserversStore.java | 6 + .../client/resource/ObjectResource.java | 112 ++- 44 files changed, 3831 insertions(+), 122 deletions(-) create mode 100644 leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/DefaultNotificationStrategy.java create mode 100644 leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/NotificationDataStore.java create mode 100644 leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/NotificationManager.java create mode 100644 leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/NotificationStrategy.java create mode 100644 leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/CriteriaBasedOnValueChecker.java create mode 100644 leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/FloatChecker.java create mode 100644 leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/IntegerChecker.java create mode 100644 leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/UnsignedIntegerChecker.java create mode 100644 leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/NotificationSender.java create mode 100644 leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/InvalidAttributesException.java create mode 100644 leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/NotificationAttributeTree.java create mode 100644 leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeBootstrapTest.java create mode 100644 leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeDiscoverTest.java create mode 100644 leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeFailedTest.java create mode 100644 leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeHouseKeepingTest.java create mode 100644 leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeObserveTest.java create mode 100644 leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/ObserversListener.java diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/endpoint/CaliforniumClientEndpointsProvider.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/endpoint/CaliforniumClientEndpointsProvider.java index 82f115f776..3962e0b965 100644 --- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/endpoint/CaliforniumClientEndpointsProvider.java +++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/endpoint/CaliforniumClientEndpointsProvider.java @@ -40,6 +40,7 @@ import org.eclipse.leshan.client.endpoint.ClientEndpointToolbox; import org.eclipse.leshan.client.endpoint.LwM2mClientEndpoint; import org.eclipse.leshan.client.endpoint.LwM2mClientEndpointsProvider; +import org.eclipse.leshan.client.notification.NotificationManager; import org.eclipse.leshan.client.request.DownlinkRequestReceiver; import org.eclipse.leshan.client.resource.LwM2mObjectTree; import org.eclipse.leshan.client.servers.LwM2mServer; @@ -146,7 +147,7 @@ public LwM2mServer extractIdentity(Exchange exchange, IpPeer foreignPeer) { @Override public void init(LwM2mObjectTree objectTree, DownlinkRequestReceiver requestReceiver, - ClientEndpointToolbox toolbox) { + NotificationManager notificationManager, ClientEndpointToolbox toolbox) { this.objectTree = objectTree; // create coap server @@ -160,7 +161,7 @@ protected Resource createRoot() { // create resources List resources = messagetranslator.createResources(coapServer, identityHandlerProvider, - identityExtrator, requestReceiver, toolbox, objectTree); + identityExtrator, requestReceiver, notificationManager, toolbox, objectTree); coapServer.add(resources.toArray(new Resource[resources.size()])); } diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/endpoint/ClientCoapMessageTranslator.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/endpoint/ClientCoapMessageTranslator.java index a718b69566..aa682c442c 100644 --- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/endpoint/ClientCoapMessageTranslator.java +++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/endpoint/ClientCoapMessageTranslator.java @@ -30,6 +30,7 @@ import org.eclipse.leshan.client.californium.request.CoapRequestBuilder; import org.eclipse.leshan.client.californium.request.LwM2mResponseBuilder; import org.eclipse.leshan.client.endpoint.ClientEndpointToolbox; +import org.eclipse.leshan.client.notification.NotificationManager; import org.eclipse.leshan.client.request.DownlinkRequestReceiver; import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; import org.eclipse.leshan.client.resource.LwM2mObjectTree; @@ -89,7 +90,7 @@ public void resourceChanged(LwM2mPath... paths) { public List createResources(CoapServer coapServer, IdentityHandlerProvider identityHandlerProvider, ServerIdentityExtractor identityExtrator, DownlinkRequestReceiver requestReceiver, - ClientEndpointToolbox toolbox, LwM2mObjectTree objectTree) { + NotificationManager notificationManager, ClientEndpointToolbox toolbox, LwM2mObjectTree objectTree) { ArrayList resources = new ArrayList<>(); // create bootstrap resource @@ -97,8 +98,8 @@ public List createResources(CoapServer coapServer, IdentityHandlerProv // create object resources for (LwM2mObjectEnabler enabler : objectTree.getObjectEnablers().values()) { - resources.add( - createObjectResource(enabler, identityHandlerProvider, identityExtrator, requestReceiver, toolbox)); + resources.add(createObjectResource(enabler, identityHandlerProvider, identityExtrator, requestReceiver, + notificationManager, toolbox)); } // link resource to object tree @@ -106,7 +107,7 @@ public List createResources(CoapServer coapServer, IdentityHandlerProv @Override public void objectAdded(LwM2mObjectEnabler object) { CoapResource clientObject = createObjectResource(object, identityHandlerProvider, identityExtrator, - requestReceiver, toolbox); + requestReceiver, notificationManager, toolbox); coapServer.add(clientObject); } @@ -124,9 +125,10 @@ public void objectRemoved(LwM2mObjectEnabler object) { public CoapResource createObjectResource(LwM2mObjectEnabler objectEnabler, IdentityHandlerProvider identityHandlerProvider, ServerIdentityExtractor identityExtractor, - DownlinkRequestReceiver requestReceiver, ClientEndpointToolbox toolbox) { + DownlinkRequestReceiver requestReceiver, NotificationManager notificationManager, + ClientEndpointToolbox toolbox) { ObjectResource objectResource = new ObjectResource(objectEnabler.getId(), identityHandlerProvider, - identityExtractor, requestReceiver, toolbox); + identityExtractor, requestReceiver, notificationManager, toolbox); objectEnabler.addListener(objectResource); return objectResource; } diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ObjectResource.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ObjectResource.java index 26fe4e375c..f102c543af 100644 --- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ObjectResource.java +++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/object/ObjectResource.java @@ -28,17 +28,23 @@ import org.eclipse.californium.core.coap.CoAP.ResponseCode; import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; +import org.eclipse.californium.core.server.resources.ResourceObserverAdapter; import org.eclipse.leshan.client.californium.LwM2mClientCoapResource; import org.eclipse.leshan.client.californium.endpoint.ServerIdentityExtractor; import org.eclipse.leshan.client.endpoint.ClientEndpointToolbox; +import org.eclipse.leshan.client.notification.NotificationManager; import org.eclipse.leshan.client.request.DownlinkRequestReceiver; import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.resource.NotificationSender; import org.eclipse.leshan.client.resource.listener.ObjectListener; import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.californium.identity.IdentityHandlerProvider; import org.eclipse.leshan.core.link.attributes.InvalidAttributeException; +import org.eclipse.leshan.core.link.lwm2m.attributes.InvalidAttributesException; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; import org.eclipse.leshan.core.node.InvalidLwM2mPathException; import org.eclipse.leshan.core.node.LwM2mNode; @@ -75,22 +81,57 @@ import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.core.response.WriteAttributesResponse; import org.eclipse.leshan.core.response.WriteResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A CoAP {@link Resource} in charge of handling requests targeting a lwM2M Object. */ public class ObjectResource extends LwM2mClientCoapResource implements ObjectListener { + private static final Logger LOG = LoggerFactory.getLogger(ObjectResource.class); + protected DownlinkRequestReceiver requestReceiver; protected ClientEndpointToolbox toolbox; + protected NotificationManager notificationManager; public ObjectResource(int objectId, IdentityHandlerProvider identityHandlerProvider, ServerIdentityExtractor serverIdentityExtractor, DownlinkRequestReceiver requestReceiver, - ClientEndpointToolbox toolbox) { + NotificationManager notificationManager, ClientEndpointToolbox toolbox) { super(Integer.toString(objectId), identityHandlerProvider, serverIdentityExtractor); this.requestReceiver = requestReceiver; + this.notificationManager = notificationManager; this.toolbox = toolbox; setObservable(true); + + this.addObserver(new ResourceObserverAdapter() { + + @Override + public void removedObserveRelation(ObserveRelation relation) { + // Get object URI + Request request = relation.getExchange().getRequest(); + String URI = request.getOptions().getUriPathString(); + // we don't manage observation on root path + if (URI == null) + return; + + // Get Server identity + LwM2mServer extractIdentity = extractIdentity(relation.getExchange(), request); + + // handle content format for Read and Observe Request + ContentFormat requestedContentFormat = null; + if (request.getOptions().hasAccept()) { + // If an request ask for a specific content format, use it (if we support it) + requestedContentFormat = ContentFormat.fromCode(request.getOptions().getAccept()); + } + + // Create Observe request + ObserveRequest observeRequest = new ObserveRequest(requestedContentFormat, URI, request); + + // Remove notification data for this request + notificationManager.clear(extractIdentity, observeRequest); + } + }); } @Override @@ -143,16 +184,44 @@ public void handleGET(CoapExchange exchange) { // Manage Observe Request if (exchange.getRequestOptions().hasObserve()) { ObserveRequest observeRequest = new ObserveRequest(requestedContentFormat, URI, coapRequest); - ObserveResponse response = requestReceiver.requestReceived(server, observeRequest).getResponse(); - if (response.getCode() == org.eclipse.leshan.core.ResponseCode.CONTENT) { - LwM2mPath path = getPath(URI); - LwM2mNode content = response.getContent(); - ContentFormat format = getContentFormat(observeRequest, requestedContentFormat); - exchange.respond(ResponseCode.CONTENT, - toolbox.getEncoder().encode(content, format, path, toolbox.getModel()), format.getCode()); - return; + + boolean isObserveRelationEstablishement = coapRequest.isObserve() + && (exchange.advanced().getRelation() == null + || !exchange.advanced().getRelation().isEstablished()); + boolean isActiveObserveCancellation = coapRequest.isObserveCancel(); + if (isObserveRelationEstablishement || isActiveObserveCancellation) { + // Handle observe request + ObserveResponse response = requestReceiver.requestReceived(server, observeRequest).getResponse(); + if (response.getCode() == org.eclipse.leshan.core.ResponseCode.CONTENT) { + LwM2mPath path = getPath(URI); + LwM2mNode content = response.getContent(); + ContentFormat format = getContentFormat(observeRequest, requestedContentFormat); + + // change notification manager state + if (isObserveRelationEstablishement) { + try { + notificationManager.initRelation(server, observeRequest, content, + createNotificationSender(exchange, server, observeRequest, + requestedContentFormat)); + } catch (InvalidAttributesException e) { + exchange.respond( + toCoapResponseCode(org.eclipse.leshan.core.ResponseCode.INTERNAL_SERVER_ERROR), + "Invalid Attributes state : " + e.getMessage()); + } + } + + // send response + exchange.respond(ResponseCode.CONTENT, + toolbox.getEncoder().encode(content, format, path, toolbox.getModel()), + format.getCode()); + } else { + exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage()); + return; + } } else { - exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage()); + // Handle notifications + notificationManager.notificationTriggered(server, observeRequest, + createNotificationSender(exchange, server, observeRequest, requestedContentFormat)); return; } } else { @@ -194,6 +263,39 @@ public void handleGET(CoapExchange exchange) { } } + protected NotificationSender createNotificationSender(CoapExchange exchange, LwM2mServer server, + ObserveRequest observeRequest, ContentFormat requestedContentFormat) { + return new NotificationSender() { + @Override + public boolean sendNotification(ObserveResponse response) { + try { + if (exchange.advanced().getRelation() != null && !exchange.advanced().getRelation().isCanceled()) { + if (response.getCode() == org.eclipse.leshan.core.ResponseCode.CONTENT) { + LwM2mPath path = observeRequest.getPath(); + LwM2mNode content = response.getContent(); + ContentFormat format = getContentFormat(observeRequest, requestedContentFormat); + Response coapResponse = new Response(ResponseCode.CONTENT); + coapResponse + .setPayload(toolbox.getEncoder().encode(content, format, path, toolbox.getModel())); + coapResponse.getOptions().setContentFormat(format.getCode()); + exchange.respond(coapResponse); + return true; + } else { + exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage()); + return false; + } + } + return false; + } catch (Exception e) { + LOG.error("Exception while sending notification [{}] for [{}] to {}", response, observeRequest, + server, e); + exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR, "failure sending notification"); + return false; + } + } + }; + } + protected ContentFormat getContentFormat(DownlinkRequest request, ContentFormat requestedContentFormat) { if (requestedContentFormat != null) { // we already check before this content format is supported. diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/LeshanClient.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/LeshanClient.java index 2e64e2fa7f..b39cd53a6f 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/LeshanClient.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/LeshanClient.java @@ -34,6 +34,10 @@ import org.eclipse.leshan.client.endpoint.LwM2mClientEndpointsProvider; import org.eclipse.leshan.client.engine.RegistrationEngine; import org.eclipse.leshan.client.engine.RegistrationEngineFactory; +import org.eclipse.leshan.client.notification.DefaultNotificationStrategy; +import org.eclipse.leshan.client.notification.NotificationDataStore; +import org.eclipse.leshan.client.notification.NotificationManager; +import org.eclipse.leshan.client.notification.NotificationStrategy; import org.eclipse.leshan.client.observer.LwM2mClientObserver; import org.eclipse.leshan.client.observer.LwM2mClientObserverAdapter; import org.eclipse.leshan.client.observer.LwM2mClientObserverDispatcher; @@ -56,6 +60,7 @@ import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.codec.LwM2mDecoder; import org.eclipse.leshan.core.node.codec.LwM2mEncoder; +import org.eclipse.leshan.core.request.BootstrapRequest; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.response.ErrorCallback; import org.eclipse.leshan.core.response.ResponseCallback; @@ -82,6 +87,7 @@ public class LeshanClient implements LwM2mClient { private final RegistrationEngine engine; private final LwM2mClientObserverDispatcher observers; private final DataSenderManager dataSenderManager; + private final NotificationManager notificationManager; public LeshanClient(String endpoint, List objectEnablers, List dataSenders, List trustStore, RegistrationEngineFactory engineFactory, @@ -120,7 +126,29 @@ public LeshanClient(String endpoint, List objectEn engine); createRegistrationUpdateHandler(engine, endpointsManager, bootstrapHandler, objectTree, linkFormatHelper); - endpointsProvider.init(objectTree, requestReceiver, toolbox); + notificationManager = createNotificationManager(objectTree, requestReceiver, sharedExecutor); + endpointsProvider.init(objectTree, requestReceiver, notificationManager, toolbox); + } + + protected NotificationManager createNotificationManager(LwM2mObjectTree objectTree, + DownlinkRequestReceiver requestReceiver, ScheduledExecutorService sharedExecutor) { + final NotificationManager notificationManager = new NotificationManager(objectTree, requestReceiver, + createNotificationStore(), createNotificationStrategy(), sharedExecutor); + this.addObserver(new LwM2mClientObserverAdapter() { + @Override + public void onBootstrapStarted(LwM2mServer bsserver, BootstrapRequest request) { + notificationManager.clear(); + } + }); + return notificationManager; + } + + protected NotificationDataStore createNotificationStore() { + return new NotificationDataStore(); + } + + protected NotificationStrategy createNotificationStrategy() { + return new DefaultNotificationStrategy(); } protected LwM2mRootEnabler createRootEnabler(LwM2mObjectTree tree) { diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/endpoint/DefaultCompositeClientEndpointsProvider.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/endpoint/DefaultCompositeClientEndpointsProvider.java index 64e8192e12..4f4452568d 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/endpoint/DefaultCompositeClientEndpointsProvider.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/endpoint/DefaultCompositeClientEndpointsProvider.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.List; +import org.eclipse.leshan.client.notification.NotificationManager; import org.eclipse.leshan.client.request.DownlinkRequestReceiver; import org.eclipse.leshan.client.resource.LwM2mObjectTree; import org.eclipse.leshan.client.servers.LwM2mServer; @@ -46,9 +47,9 @@ public DefaultCompositeClientEndpointsProvider(Collection trustStore, ClientEndpointToolbox toolbox); diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/DefaultNotificationStrategy.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/DefaultNotificationStrategy.java new file mode 100644 index 0000000000..a729069b44 --- /dev/null +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/DefaultNotificationStrategy.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2024 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.client.notification; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.leshan.client.notification.checker.CriteriaBasedOnValueChecker; +import org.eclipse.leshan.client.notification.checker.FloatChecker; +import org.eclipse.leshan.client.notification.checker.IntegerChecker; +import org.eclipse.leshan.client.notification.checker.UnsignedIntegerChecker; +import org.eclipse.leshan.core.link.lwm2m.attributes.InvalidAttributesException; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeModel; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.link.lwm2m.attributes.NotificationAttributeTree; +import org.eclipse.leshan.core.model.ResourceModel.Type; +import org.eclipse.leshan.core.node.LwM2mChildNode; +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.LwM2mResourceInstance; +import org.eclipse.leshan.core.node.LwM2mSingleResource; + +/** + * Default implementation of {@link NotificationStrategy} + */ +public class DefaultNotificationStrategy implements NotificationStrategy { + + private final Map checkers; + + public static Map createDefaultCheckers() { + Map checkers = new HashMap<>(); + checkers.put(Type.FLOAT, new FloatChecker()); + checkers.put(Type.INTEGER, new IntegerChecker()); + checkers.put(Type.UNSIGNED_INTEGER, new UnsignedIntegerChecker()); + return checkers; + } + + public DefaultNotificationStrategy() { + this(createDefaultCheckers()); + } + + public DefaultNotificationStrategy(Map checkers) { + this.checkers = checkers; + } + + @Override + public NotificationAttributeTree selectNotificationsAttributes(LwM2mPath path, NotificationAttributeTree attributes) + throws InvalidAttributesException { + + LwM2mAttributeSet set = attributes.getWithInheritance(path); + if (set == null || set.isEmpty()) { + return null; + } + set.validate(path, null); + + NotificationAttributeTree result = new NotificationAttributeTree(); + result.put(path, set); + return result; + } + + @Override + public boolean hasAttribute(NotificationAttributeTree attributes, LwM2mPath path, LwM2mAttributeModel model) { + return getAttributeValue(attributes, path, model) != null; + } + + @Override + public T getAttributeValue(NotificationAttributeTree attributes, LwM2mPath path, LwM2mAttributeModel model) { + LwM2mAttributeSet set = attributes.get(path); + if (set == null) + return null; + else { + LwM2mAttribute attr = set.get(model); + if (attr == null) + return null; + else + return attr.getValue(); + } + } + + @Override + public boolean hasCriteriaBasedOnValue(NotificationAttributeTree attributes, LwM2mPath path) { + LwM2mAttributeSet set = attributes.get(path); + return set != null && (set.contains(LwM2mAttributes.GREATER_THAN) || set.contains(LwM2mAttributes.LESSER_THAN) + || set.contains(LwM2mAttributes.STEP)); + } + + @Override + public boolean shouldTriggerNotificationBasedOnValueChange(NotificationAttributeTree attributes, LwM2mPath path, + LwM2mNode lastSentNode, LwM2mChildNode newNode) { + + // Get Previous and New Values + Object lastSentValue; + Object newValue; + Type resourceType; + if (lastSentNode instanceof LwM2mSingleResource && newNode instanceof LwM2mSingleResource) { + lastSentValue = ((LwM2mSingleResource) lastSentNode).getValue(); + newValue = ((LwM2mSingleResource) newNode).getValue(); + resourceType = ((LwM2mSingleResource) newNode).getType(); + } else if (lastSentNode instanceof LwM2mResourceInstance && newNode instanceof LwM2mResourceInstance) { + lastSentValue = ((LwM2mResourceInstance) lastSentNode).getValue(); + newValue = ((LwM2mResourceInstance) newNode).getValue(); + resourceType = ((LwM2mResourceInstance) newNode).getType(); + } else { + throw new IllegalArgumentException(String.format( + "Unexpected nodes (last send node %s new value %s) for check about value changed, only LwM2mSingleResource or LwM2mResourceInstance are supported", + lastSentNode.getClass().getSimpleName(), newNode.getClass().getSimpleName())); + } + + // Check criteria + CriteriaBasedOnValueChecker checker = checkers.get(resourceType); + if (checker == null) { + throw new IllegalArgumentException( + String.format("Unexpected resource type : %s is not supported", resourceType)); + } + LwM2mAttributeSet set = attributes.get(path); + return checker.shouldTriggerNotificationBasedOnValueChange(set, lastSentValue, newValue); + } + +} diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/NotificationDataStore.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/NotificationDataStore.java new file mode 100644 index 0000000000..3428d799fc --- /dev/null +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/NotificationDataStore.java @@ -0,0 +1,228 @@ +/******************************************************************************* + * 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.client.notification; + +import java.util.Iterator; +import java.util.NavigableMap; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.ScheduledFuture; + +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.link.lwm2m.attributes.NotificationAttributeTree; +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.util.Validate; + +/** + * This class store information needed to handle write attributes behavior. + *

+ * It is used by {@link NotificationManager} + */ +public class NotificationDataStore { + + private final NavigableMap store = new TreeMap<>(); + + public NotificationData getNotificationData(LwM2mServer server, ObserveRequest request) { + return store.get(toKey(server, request)); + } + + public synchronized NotificationData addNotificationData(LwM2mServer server, ObserveRequest request, + NotificationData data) { + NotificationData previousData = store.put(toKey(server, request), data); + if (previousData != null) { + // cancel task of previous data + cancelTasks(previousData); + } + return previousData; + } + + public synchronized NotificationData updateNotificationData(LwM2mServer server, ObserveRequest request, + NotificationData data) { + + NotificationData previousData = store.replace(toKey(server, request), data); + if (previousData != null) { + // If updated, cancel task of previous data + cancelTasks(previousData); + } else { + // If NOT updated, cancel task of given data + cancelTasks(data); + } + return previousData; + } + + public synchronized void removeNotificationData(LwM2mServer server, ObserveRequest request) { + NotificationData removed = store.remove(toKey(server, request)); + if (removed != null) { + cancelTasks(removed); + } + } + + public synchronized void clearAllNotificationDataUnder(LwM2mPath parentPath) { + Iterator it = store.keySet().iterator(); + while (it.hasNext()) { + NotificationDataKey key = it.next(); + if (key.getPath().startWith(parentPath)) { + it.remove(); + } + } + } + + public synchronized void clearAllNotificationDataFor(LwM2mServer server) { + SortedMap toRemove = store.subMap(floorKeyFor(server), + ceilKeyFor(server)); + for (NotificationData toRemoveData : toRemove.values()) { + cancelTasks(toRemoveData); + } + toRemove.clear(); + } + + public synchronized void clearAllNotificationData() { + for (NotificationData toRemoveData : store.values()) { + cancelTasks(toRemoveData); + } + store.clear(); + } + + public synchronized boolean isEmpty() { + return store.isEmpty(); + } + + private void cancelTasks(NotificationData data) { + if (data.getPminFuture() != null) { + data.getPminFuture().cancel(false); + } + if (data.getPmaxFuture() != null) { + data.getPmaxFuture().cancel(false); + } + } + + private NotificationDataKey floorKeyFor(LwM2mServer server) { + // TODO should be replaced by a ObservationRelationIdentifier probably based on Token + return new NotificationDataKey(server.getId(), LwM2mPath.ROOTPATH); + } + + private NotificationDataKey ceilKeyFor(LwM2mServer server) { + // TODO should be replaced by a ObservationRelationIdentifier probably based on Token + return new NotificationDataKey(server.getId() + 1, LwM2mPath.ROOTPATH); + } + + private NotificationDataKey toKey(LwM2mServer server, ObserveRequest request) { + // TODO should be replaced by a ObservationRelationIdentifier probably based on Token + return new NotificationDataKey(server.getId(), request.getPath()); + } + + private static class NotificationDataKey implements Comparable { + + private final Long serverId; + // TODO should be replaced by a ObservationRelationIdentifier probably based on Token + private final LwM2mPath path; + + public NotificationDataKey(Long serverId, LwM2mPath path) { + Validate.notNull(serverId); + Validate.notNull(path); + this.serverId = serverId; + this.path = path; + } + + @Override + public int compareTo(NotificationDataKey o) { + // check for null + if (o == null) { + // object can not be null following Comparable javadoc + throw new NullPointerException(); + } + + // compare server Id + int r = getServerId().compareTo(o.getServerId()); + if (r != 0) + return r; + + // if server id equals, then compare path + return path.compareTo(o.getPath()); + } + + public Long getServerId() { + return serverId; + } + + public LwM2mPath getPath() { + return path; + } + } + + public static class NotificationData { + private final NotificationAttributeTree attributes; + private final Long lastSendingTime; // time of last sent notification + private final LwM2mNode lastSentValue; // last value sent + private final ScheduledFuture pminTask; // task which will send delayed notification for pmin. + private final ScheduledFuture pmaxTask; // task which will send delayed notification for pmax. + + public NotificationData(NotificationAttributeTree attributes, Long lastSendingTime, LwM2mNode lastSentValue, + ScheduledFuture nextNotification) { + this.attributes = attributes; + this.lastSendingTime = lastSendingTime; + this.lastSentValue = lastSentValue; + this.pminTask = null; + this.pmaxTask = nextNotification; + } + + public NotificationData(NotificationData previous, ScheduledFuture pminTask) { + this.attributes = previous.getAttributes(); + this.lastSendingTime = previous.getLastSendingTime(); + this.lastSentValue = previous.getLastSentValue(); + this.pminTask = pminTask; + this.pmaxTask = previous.getPmaxFuture(); + } + + public NotificationAttributeTree getAttributes() { + return attributes; + } + + public Long getLastSendingTime() { + return lastSendingTime; + } + + public LwM2mNode getLastSentValue() { + return lastSentValue; + } + + public ScheduledFuture getPminFuture() { + return pminTask; + } + + public ScheduledFuture getPmaxFuture() { + return pmaxTask; + } + + public boolean usePmin() { + return lastSendingTime != null; + } + + public boolean usePmax() { + return pmaxTask != null; + } + + public boolean hasCriteriaBasedOnValue() { + return lastSentValue != null; + } + + public boolean pminTaskScheduled() { + return pminTask != null; + } + } +} diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/NotificationManager.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/NotificationManager.java new file mode 100644 index 0000000000..1227417060 --- /dev/null +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/NotificationManager.java @@ -0,0 +1,272 @@ +/******************************************************************************* + * 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.client.notification; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.leshan.client.notification.NotificationDataStore.NotificationData; +import org.eclipse.leshan.client.request.DownlinkRequestReceiver; +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.resource.LwM2mObjectTree; +import org.eclipse.leshan.client.resource.NotificationSender; +import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.link.lwm2m.attributes.InvalidAttributesException; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.link.lwm2m.attributes.NotificationAttributeTree; +import org.eclipse.leshan.core.node.LwM2mChildNode; +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.response.ObserveResponse; +import org.eclipse.leshan.core.util.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is responsible to modify default observe behavior based on write attributes. + *

+ * It does not support Observe-Composite. + */ +public class NotificationManager { + private final Logger LOG = LoggerFactory.getLogger(NotificationManager.class); + + private final DownlinkRequestReceiver receiver; + private final NotificationDataStore store; + private final LwM2mObjectTree objectTree; + private final NotificationStrategy strategy; + private final ScheduledExecutorService executor; + private final boolean executorAttached; + + public NotificationManager(LwM2mObjectTree objectTree, DownlinkRequestReceiver requestReceiver) { + this(objectTree, requestReceiver, new NotificationDataStore(), new DefaultNotificationStrategy(), null); + } + + public NotificationManager(LwM2mObjectTree objectTree, DownlinkRequestReceiver requestReceiver, + NotificationDataStore store, NotificationStrategy strategy, ScheduledExecutorService executor) { + Validate.notNull(objectTree); + Validate.notNull(requestReceiver); + Validate.notNull(store); + Validate.notNull(strategy); + + this.objectTree = objectTree; + this.receiver = requestReceiver; + this.store = store; + this.strategy = strategy; + + if (executor == null) { + this.executor = Executors.newSingleThreadScheduledExecutor(); + this.executorAttached = true; + } else { + this.executor = executor; + this.executorAttached = false; + } + this.objectTree.addListener(new ObjectsListenerAdapter() { + @Override + public void objectRemoved(LwM2mObjectEnabler object) { + // I guess ideally this SHOULD NOT be needed because + // If an observed resource under this object is removed then a 4.04 notification should be sent + // immediately. + // but underlying library doesn't really implement this, e.g : + // https://github.com/eclipse-californium/californium/issues/2223 + store.clearAllNotificationDataUnder(new LwM2mPath(object.getId())); + } + }); + } + + public synchronized void initRelation(LwM2mServer server, ObserveRequest request, LwM2mNode node, + NotificationSender sender) throws InvalidAttributesException { + // Get Attributes for this (server, request) + LwM2mObjectEnabler objectEnabler = objectTree.getObjectEnabler(request.getPath().getObjectId()); + if (objectEnabler == null) + return; // no object enabler : nothing to observe + NotificationAttributeTree attributes = objectEnabler.getAttributesFor(server); + if (attributes != null && !attributes.isEmpty()) { + attributes = strategy.selectNotificationsAttributes(request.getPath(), attributes); + } + + // If there is no attributes this is just classic observe so nothing to do. + if (attributes == null || attributes.isEmpty()) + return; + + LOG.debug("Handle observe relation for {} / {}", server, request); + + // Store needed data for this observe relation. + updateNotificationData(true, server, request, attributes, node, sender); + } + + public synchronized void notificationTriggered(LwM2mServer server, ObserveRequest request, + NotificationSender sender) { + LOG.trace("Notification triggered for observe relation of {} / {}", server, request); + + // Get Notification Data for given server / request + NotificationData notificationData = store.getNotificationData(server, request); + if (notificationData == null) { + // if there no notification data, this is classic observe (without notification attributes) + ObserveResponse observeResponse = createResponse(server, request); + sender.sendNotification(observeResponse); + return; + } + + // ELSE handle Notification Attributes. + NotificationAttributeTree attributes = notificationData.getAttributes(); + ObserveResponse candidateNotificationToSend = null; + + // Handle if pmin = pmax, we don't need to check anything only send notification each pmin=pmax seconds + // AFAWK, this case is not clearly defined in LWM2M v1.1.1 or in its references but we can find hints in : + // https://datatracker.ietf.org/doc/html/draft-ietf-core-conditional-attributes-06#section-4 + // referenced by LWM2M v1.2.1 + if (notificationData.usePmax()) { + Long pmin = strategy.getAttributeValue(attributes, request.getPath(), LwM2mAttributes.MINIMUM_PERIOD); + Long pmax = strategy.getAttributeValue(attributes, request.getPath(), LwM2mAttributes.MAXIMUM_PERIOD); + if (pmax == pmin) { + // we only send notification when pmax timer is reached. + return; + } + } + + // if there is criteria based on value + if (notificationData.hasCriteriaBasedOnValue()) { + candidateNotificationToSend = createResponse(server, request); + if (candidateNotificationToSend.isSuccess()) { + LwM2mChildNode newValue = candidateNotificationToSend.getContent(); + + // if criteria doesn't match do not raise any event. + if (!strategy.shouldTriggerNotificationBasedOnValueChange(attributes, request.getPath(), + notificationData.getLastSentValue(), newValue)) { + return; + } + } + // else if there is an error send notification now. + } + + // If PMIN is used check if we need to delay this notification. + if (notificationData.usePmin()) { + LOG.trace("handle pmin for observe relation of {} / {}", server, request); + + if (notificationData.pminTaskScheduled()) { + // nothing to do if a task is already scheduled + return; + } + + // calculate time since last notification + Long timeSinceLastNotification = TimeUnit.SECONDS + .convert(System.nanoTime() - notificationData.getLastSendingTime(), TimeUnit.NANOSECONDS); + Long pmin = strategy.getAttributeValue(attributes, request.getPath(), LwM2mAttributes.MINIMUM_PERIOD); + if (timeSinceLastNotification < pmin) { + ScheduledFuture pminTask = executor.schedule(new Callable() { + @Override + public Void call() throws Exception { + sendNotification(server, request, null, attributes, sender); + return null; + } + }, pmin - timeSinceLastNotification, TimeUnit.SECONDS); + // schedule next task for pmin but do not send notification + store.updateNotificationData(server, request, new NotificationData(notificationData, pminTask)); + return; + } + } + + sendNotification(server, request, candidateNotificationToSend, attributes, sender); + } + + public synchronized void clear(LwM2mServer server, ObserveRequest request) { + // remove all data about observe relation for given server / request. + store.removeNotificationData(server, request); + } + + public synchronized void clear(LwM2mServer server) { + // remove all data about observe relation for given server. + store.clearAllNotificationDataFor(server); + } + + public synchronized void clear() { + // remove all data about observe relation. + store.clearAllNotificationData(); + } + + protected synchronized void updateNotificationData(boolean newRelation, LwM2mServer server, ObserveRequest request, + NotificationAttributeTree attributes, LwM2mNode newValue, NotificationSender sender) { + // Get Request Path + LwM2mPath path = request.getPath(); + + // Store last sending time if needed + Long lastSendingTime = null; + if (strategy.hasAttribute(attributes, path, LwM2mAttributes.MINIMUM_PERIOD)) { + lastSendingTime = System.nanoTime(); + } + + // Store last value sent if needed; + LwM2mNode lastValue = null; + if (strategy.hasCriteriaBasedOnValue(attributes, path)) { + lastValue = newValue; + } + + // Schedule notification for Max Period if needed + ScheduledFuture pmaxTask = null; + if (strategy.hasAttribute(attributes, path, LwM2mAttributes.MAXIMUM_PERIOD)) { + pmaxTask = executor.schedule(new Callable() { + @Override + public Void call() throws Exception { + sendNotification(server, request, null, attributes, sender); + return null; + } + }, strategy.getAttributeValue(attributes, path, LwM2mAttributes.MAXIMUM_PERIOD), TimeUnit.SECONDS); + } + + // Create State for this observe relation + if (newRelation) { + store.addNotificationData(server, request, + new NotificationData(attributes, lastSendingTime, lastValue, pmaxTask)); + } else { + store.updateNotificationData(server, request, + new NotificationData(attributes, lastSendingTime, lastValue, pmaxTask)); + } + } + + protected void sendNotification(LwM2mServer server, ObserveRequest request, ObserveResponse observeResponse, + NotificationAttributeTree attributes, NotificationSender sender) { + if (observeResponse == null) { + observeResponse = createResponse(server, request); + } + if (!sender.sendNotification(observeResponse)) { + // remove data as relation doesn't exist anymore + clear(server, request); + } else { + if (observeResponse.isFailure()) { + // remove data as relation must be removed on failure + clear(server, request); + } else { + updateNotificationData(false, server, request, attributes, observeResponse.getContent(), sender); + } + } + } + + protected ObserveResponse createResponse(LwM2mServer server, ObserveRequest request) { + // TODO write attributes : maybe we can remove "receiver" dependencie and directly use ObjectTree ? + return receiver.requestReceived(server, request).getResponse(); + } + + public void destroy() { + if (executorAttached) { + executor.shutdownNow(); + } + } +} diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/NotificationStrategy.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/NotificationStrategy.java new file mode 100644 index 0000000000..125972b101 --- /dev/null +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/NotificationStrategy.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2024 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.client.notification; + +import org.eclipse.leshan.core.link.lwm2m.attributes.InvalidAttributesException; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeModel; +import org.eclipse.leshan.core.link.lwm2m.attributes.NotificationAttributeTree; +import org.eclipse.leshan.core.node.LwM2mChildNode; +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; + +/** + * This class is responsible to implement how write attribute are assigned to given LWM2M node and how it should be + * handle. + *

+ * This aims to provide some flexibility because LWM2M specification isn't really clear on this topic. + * + * @see Discussion about Write + * Attributes on OMA LwM2M for Developers Github repository + * @see DefaultNotificationStrategy + */ +public interface NotificationStrategy { + + /** + * Create a {@link NotificationAttributeTree} when observe relation is initiated with information needed later. + * + * @param path The path of the LWM2M node to observe + * @param attributes The whole {@link NotificationAttributeTree} currently attached to the object targeted by the + * given path. + * + * @throws InvalidAttributesException if current attributes configuration is inconsistent. + */ + NotificationAttributeTree selectNotificationsAttributes(LwM2mPath path, NotificationAttributeTree attributes) + throws InvalidAttributesException; + + /** + * @param attributes The attributes returned by + * {@link #selectNotificationsAttributes(LwM2mPath, NotificationAttributeTree)} + * @param path The path of the LWM2M node to observe + * @param model The {@link LwM2mAttributeModel} for which we want to know if the given attribute is assigned + */ + boolean hasAttribute(NotificationAttributeTree attributes, LwM2mPath path, LwM2mAttributeModel model); + + /** + * @param attributes The attributes returned by + * {@link #selectNotificationsAttributes(LwM2mPath, NotificationAttributeTree)} + * @param path The path of the LWM2M node to observe + * @param model he {@link LwM2mAttributeModel} for which we want to get the value + */ + T getAttributeValue(NotificationAttributeTree attributes, LwM2mPath path, LwM2mAttributeModel model); + + /** + * @param attributes The attributes returned by + * {@link #selectNotificationsAttributes(LwM2mPath, NotificationAttributeTree)} + * @param path The path of the LWM2M node to observe + * @return True some attributes assigned are based on value (e.g. gt, lt, st) + */ + boolean hasCriteriaBasedOnValue(NotificationAttributeTree attributes, LwM2mPath path); + + /** + * @param attributes The attributes returned by + * {@link #selectNotificationsAttributes(LwM2mPath, NotificationAttributeTree)} + * @param path The path of the LWM2M node to observe + * @param lastSentNode The last LwM2mNode sent in a notification for this observe relation. + * @param newNode The a new value for which we should decide if a new notification should be sent following criteria + * based on value. + * @return True if a new notification should be sent. + */ + boolean shouldTriggerNotificationBasedOnValueChange(NotificationAttributeTree attributes, LwM2mPath path, + LwM2mNode lastSentNode, LwM2mChildNode newNode); +} diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/CriteriaBasedOnValueChecker.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/CriteriaBasedOnValueChecker.java new file mode 100644 index 0000000000..eab243436a --- /dev/null +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/CriteriaBasedOnValueChecker.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2024 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.client.notification.checker; + +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; + +/** + * A {@link CriteriaBasedOnValueChecker} MUST evaluate new value based on Notification {@link LwM2mAttribute} and + * previous value sent to determine if new notification should be sent. + * + * @see Attribute Interactions + * + */ +public interface CriteriaBasedOnValueChecker { + /** + * @param attributes {@link LwM2mAttributeSet} attached to the node. + * @param lastSentValue value sent in previous notification. + * @param newValue value for which it should be decided if a notification should be sent. + * @return true if new notification should be sent. + */ + boolean shouldTriggerNotificationBasedOnValueChange(LwM2mAttributeSet attributes, Object lastSentValue, + Object newValue); +} diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/FloatChecker.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/FloatChecker.java new file mode 100644 index 0000000000..3bd97cd251 --- /dev/null +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/FloatChecker.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2024 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.client.notification.checker; + +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.model.ResourceModel.Type; +import org.eclipse.leshan.core.node.LwM2mResource; + +/** + * A {@link CriteriaBasedOnValueChecker} for {@link LwM2mResource} of {@link Type#FLOAT} + */ +public class FloatChecker implements CriteriaBasedOnValueChecker { + + @Override + public boolean shouldTriggerNotificationBasedOnValueChange(LwM2mAttributeSet attributes, Object lastSentValue, + Object newValue) { + Double lastSentDouble = (Double) lastSentValue; + Double newDouble = (Double) newValue; + boolean hasNumericalAttributes = false; + + if (attributes.contains(LwM2mAttributes.STEP)) { + hasNumericalAttributes = true; + + if (Math.abs(lastSentDouble - newDouble) >= attributes.get(LwM2mAttributes.STEP).getValue()) { + return true; + } + } + + if (attributes.contains(LwM2mAttributes.LESSER_THAN)) { + hasNumericalAttributes = true; + + Double lessThan = attributes.get(LwM2mAttributes.LESSER_THAN).getValue(); + if (lastSentDouble >= lessThan && newDouble < lessThan) { + return true; + } + } + + if (attributes.contains(LwM2mAttributes.GREATER_THAN)) { + hasNumericalAttributes = true; + + Double greaterThan = attributes.get(LwM2mAttributes.GREATER_THAN).getValue(); + if (lastSentDouble <= greaterThan && newDouble > greaterThan) { + return true; + } + } + + // if we have numerical attribute we can send notification else if one condition matches we already return true; + return !hasNumericalAttributes; + } +} diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/IntegerChecker.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/IntegerChecker.java new file mode 100644 index 0000000000..700812498b --- /dev/null +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/IntegerChecker.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2024 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.client.notification.checker; + +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.model.ResourceModel.Type; +import org.eclipse.leshan.core.node.LwM2mResource; + +/** + * A {@link CriteriaBasedOnValueChecker} for {@link LwM2mResource} of {@link Type#INTEGER} + */ +public class IntegerChecker implements CriteriaBasedOnValueChecker { + + @Override + public boolean shouldTriggerNotificationBasedOnValueChange(LwM2mAttributeSet attributes, Object lastSentValue, + Object newValue) { + Long lastSentLong = (Long) lastSentValue; + Long newLong = (Long) newValue; + boolean hasNumericalAttributes = false; + + if (attributes.contains(LwM2mAttributes.STEP)) { + hasNumericalAttributes = true; + + if (Math.abs(lastSentLong - newLong) >= attributes.get(LwM2mAttributes.STEP).getValue()) { + return true; + } + } + + if (attributes.contains(LwM2mAttributes.LESSER_THAN)) { + hasNumericalAttributes = true; + + long lessThan = (long) Math.ceil(attributes.get(LwM2mAttributes.LESSER_THAN).getValue()); + if (lastSentLong >= lessThan && newLong < lessThan) { + return true; + } + } + + if (attributes.contains(LwM2mAttributes.GREATER_THAN)) { + hasNumericalAttributes = true; + + long greaterThan = (long) Math.floor(attributes.get(LwM2mAttributes.GREATER_THAN).getValue()); + if (lastSentLong <= greaterThan && newLong > greaterThan) { + return true; + } + } + // if we have numerical attribute we can send notification else if one condition matches we already return true; + return !hasNumericalAttributes; + } + +} diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/UnsignedIntegerChecker.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/UnsignedIntegerChecker.java new file mode 100644 index 0000000000..ae67223e50 --- /dev/null +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/notification/checker/UnsignedIntegerChecker.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2024 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.client.notification.checker; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; + +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.model.ResourceModel.Type; +import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.util.datatype.ULong; + +/** + * A {@link CriteriaBasedOnValueChecker} for {@link LwM2mResource} of {@link Type#UNSIGNED_INTEGER} + */ +public class UnsignedIntegerChecker implements CriteriaBasedOnValueChecker { + + @Override + public boolean shouldTriggerNotificationBasedOnValueChange(LwM2mAttributeSet attributes, Object lastSentValue, + Object newValue) { + BigInteger lastSentULong = ((ULong) lastSentValue).toBigInteger(); + BigInteger newULong = ((ULong) newValue).toBigInteger(); + boolean hasNumericalAttributes = false; + + if (attributes.contains(LwM2mAttributes.STEP)) { + hasNumericalAttributes = true; + + // Handle Step + BigInteger step = new BigDecimal(attributes.get(LwM2mAttributes.STEP).getValue()) + .setScale(0, RoundingMode.CEILING).toBigIntegerExact(); + if (lastSentULong.subtract(newULong).abs().subtract(step).signum() >= 0) { + return true; + } + } + + if (attributes.contains(LwM2mAttributes.LESSER_THAN)) { + hasNumericalAttributes = true; + + // Handle LESSER_THAN + BigDecimal lessThan = new BigDecimal(attributes.get(LwM2mAttributes.LESSER_THAN).getValue()); + BigInteger lessThanRounded = lessThan.setScale(0, RoundingMode.CEILING).toBigIntegerExact(); + if (lastSentULong.compareTo(lessThanRounded) >= 0 && newULong.compareTo(lessThanRounded) < 0) { + return true; + } + } + + if (attributes.contains(LwM2mAttributes.GREATER_THAN)) { + hasNumericalAttributes = true; + + // Handle LESSER_THAN + BigDecimal lessThan = new BigDecimal(attributes.get(LwM2mAttributes.GREATER_THAN).getValue()); + BigInteger lessThanRounded = lessThan.setScale(0, RoundingMode.FLOOR).toBigIntegerExact(); + if (lastSentULong.compareTo(lessThanRounded) <= 0 && newULong.compareTo(lessThanRounded) > 0) { + return true; + } + } + + // if we have numerical attribute we can send notification else if one condition matches we already return true; + return !hasNumericalAttributes; + } +} diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseObjectEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseObjectEnabler.java index dbe60c83c1..0bf1e00c1a 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseObjectEnabler.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseObjectEnabler.java @@ -30,10 +30,14 @@ import org.eclipse.leshan.client.LwM2mClient; import org.eclipse.leshan.client.resource.listener.ObjectListener; +import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter; import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.client.util.LinkFormatHelper; import org.eclipse.leshan.core.LwM2mId; import org.eclipse.leshan.core.link.lwm2m.LwM2mLink; +import org.eclipse.leshan.core.link.lwm2m.attributes.InvalidAttributesException; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.NotificationAttributeTree; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; @@ -80,10 +84,55 @@ public abstract class BaseObjectEnabler implements LwM2mObjectEnabler { private LwM2mClient lwm2mClient; private LinkFormatHelper linkFormatHelper; + private final NotificationAttributeTree assignedAttributes = new NotificationAttributeTree(); + public BaseObjectEnabler(int id, ObjectModel objectModel) { this.id = id; this.objectModel = objectModel; this.transactionalListener = createTransactionListener(); + this.transactionalListener.addListener(new ObjectsListenerAdapter() { + + @Override + public void resourceChanged(LwM2mPath... paths) { + synchronized (BaseObjectEnabler.this) { + // Assigned attributes housekeeping : if resource instance is removed we removed attached + // attributes. + for (LwM2mPath p : paths) { + if (p.isResource()) { + ResourceModel resourceModel = objectModel.resources.get(p.getResourceId()); + if (resourceModel.multiple) { + // TODO ideally we should have a better event like 'resource instance removed' + // Maybe we should change the current event model for a more advanced one... + // Plus, ideally all next code should be done atomically... + Set resourceInstancePaths = assignedAttributes.getChildren(p); + if (!resourceInstancePaths.isEmpty()) { + List instanceResourceIds = getAvailableInstanceResourceIds( + p.getObjectInstanceId(), p.getResourceId()); + // We verify that attribute in tree are attached to an existent resource instance + for (LwM2mPath resourceInstancePath : resourceInstancePaths) { + if (!instanceResourceIds + .contains(resourceInstancePath.getResourceInstanceId())) { + assignedAttributes.remove(resourceInstancePath); + } + } + } + } + } + } + } + } + + @Override + public void objectInstancesRemoved(LwM2mObjectEnabler object, int... instanceIds) { + synchronized (BaseObjectEnabler.this) { + // Assigned attributes housekeeping : if object instance is removed we removed attached + // attributes. + for (int instanceId : instanceIds) { + assignedAttributes.removeAllUnder(new LwM2mPath(getId(), instanceId)); + } + } + } + }); } protected TransactionalObjectListener createTransactionListener() { @@ -390,9 +439,28 @@ public synchronized WriteAttributesResponse writeAttributes(LwM2mServer server, if (server.isLwm2mBootstrapServer()) { return WriteAttributesResponse.methodNotAllowed(); } - // TODO should be implemented here to be available for all object enabler - // This should be a not implemented error, but this is not defined in the spec. - return WriteAttributesResponse.internalServerError("not implemented"); + + // apply new attribute values + LwM2mAttributeSet currentAttributes = assignedAttributes.get(request.getPath()); + LwM2mAttributeSet newValue; + if (currentAttributes != null) { + newValue = currentAttributes.apply(request.getAttributes()); + } else { + newValue = request.getAttributes(); + } + try { + newValue.validate(request.getPath(), getObjectModel()); + } catch (InvalidAttributesException e) { + return WriteAttributesResponse.badRequest(e.getMessage()); + } + + if (newValue.isEmpty()) { + assignedAttributes.remove(request.getPath()); + } else { + assignedAttributes.put(request.getPath(), newValue); + } + + return WriteAttributesResponse.success(); } @Override @@ -563,4 +631,9 @@ protected void fireResourcesChanged(LwM2mPath... paths) { public ContentFormat getDefaultEncodingFormat(DownlinkRequest request) { return ContentFormat.DEFAULT; } + + @Override + public NotificationAttributeTree getAttributesFor(LwM2mServer server) { + return assignedAttributes; + } } diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectEnabler.java index 1938bc7bed..4e9de59fd0 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectEnabler.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectEnabler.java @@ -25,6 +25,7 @@ import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Stoppable; +import org.eclipse.leshan.core.link.lwm2m.attributes.NotificationAttributeTree; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.request.BootstrapDeleteRequest; import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; @@ -115,4 +116,6 @@ public interface LwM2mObjectEnabler { void endTransaction(byte level); ContentFormat getDefaultEncodingFormat(DownlinkRequest request); + + NotificationAttributeTree getAttributesFor(LwM2mServer server); } diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/NotificationSender.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/NotificationSender.java new file mode 100644 index 0000000000..1cd5fc9408 --- /dev/null +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/NotificationSender.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * 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.client.resource; + +import org.eclipse.leshan.core.response.ObserveResponse; + +public interface NotificationSender { + /** + * Send notification, return false if there is no more observe relation. + */ + boolean sendNotification(ObserveResponse response); +} diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java index 20ccc1c997..f1806cf646 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java @@ -38,7 +38,10 @@ import org.eclipse.leshan.core.link.lwm2m.LwM2mLink; import org.eclipse.leshan.core.link.lwm2m.MixedLwM2mLink; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.link.lwm2m.attributes.MixedLwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.NotificationAttributeTree; import org.eclipse.leshan.core.model.LwM2mCoreObjectVersionRegistry; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.model.ObjectModel; @@ -92,9 +95,10 @@ public int compare(LwM2mObjectEnabler o1, LwM2mObjectEnabler o2) { List availableInstance = objectEnabler.getAvailableInstanceIds(); // Include an object link if there are no instances or there are object attributes (e.g. "ver") - List> objectAttributes = getObjectAttributes(objectEnabler.getObjectModel()); - if (availableInstance.isEmpty() || (objectAttributes != null)) { - links.add(new MixedLwM2mLink(rootPath, new LwM2mPath(objectEnabler.getId()), objectAttributes)); + LwM2mAttributeSet objectAttributes = getObjectAttributes(objectEnabler.getObjectModel()); + if (availableInstance.isEmpty() || (!objectAttributes.isEmpty())) { + links.add(new MixedLwM2mLink(rootPath, new LwM2mPath(objectEnabler.getId()), + new MixedLwM2mAttributeSet(objectAttributes.asCollection()))); } for (Integer instanceId : objectEnabler.getAvailableInstanceIds()) { links.add(new MixedLwM2mLink(rootPath, new LwM2mPath(objectEnabler.getId(), instanceId))); @@ -161,8 +165,10 @@ public LwM2mLink[] getObjectDescription(LwM2mServer server, LwM2mObjectEnabler o List links = new ArrayList<>(); // create link for "object" - List> objectAttributes = getObjectAttributes(objectEnabler.getObjectModel()); - links.add(new LwM2mLink(rootPath, new LwM2mPath(objectEnabler.getId()), objectAttributes)); + LwM2mPath objectPath = new LwM2mPath(objectEnabler.getId()); + LwM2mAttributeSet objectAttributes = getObjectAttributes(objectEnabler.getObjectModel()); + LwM2mAttributeSet notificationAttributes = getNotificationAttributeFor(server, objectEnabler, objectPath); + links.add(new LwM2mLink(rootPath, objectPath, objectAttributes.merge(notificationAttributes))); // create links for each available instance if (depth.isDeeperOrEqualThan(LwM2mNodeLevel.OBJECT_INSTANCE)) { @@ -255,7 +261,9 @@ public LwM2mLink[] getInstanceDescription(LwM2mServer server, LwM2mObjectEnabler List links = new ArrayList<>(); // create link for "instance" - links.add(new LwM2mLink(rootPath, new LwM2mPath(objectEnabler.getId(), instanceId))); + LwM2mPath instancePath = new LwM2mPath(objectEnabler.getId(), instanceId); + LwM2mAttributeSet instanceAttributes = getNotificationAttributeFor(server, objectEnabler, instancePath); + links.add(new LwM2mLink(rootPath, instancePath, instanceAttributes)); // create links for each available resource if (depth.isDeeperOrEqualThan(LwM2mNodeLevel.RESOURCE)) { @@ -278,34 +286,53 @@ public LwM2mLink[] getResourceDescription(LwM2mServer server, LwM2mObjectEnabler List links = new ArrayList<>(); // create link for "resource" + LwM2mPath resourcePath = new LwM2mPath(objectEnabler.getId(), instanceId, resourceId); + LwM2mAttributeSet resourceAttributes = getNotificationAttributeFor(server, objectEnabler, resourcePath); + List availableInstanceResourceIds = objectEnabler.getAvailableInstanceResourceIds(instanceId, resourceId); if (!availableInstanceResourceIds.isEmpty()) { - links.add(new LwM2mLink(rootPath, new LwM2mPath(objectEnabler.getId(), instanceId, resourceId), - LwM2mAttributes.create(LwM2mAttributes.DIMENSION, (long) availableInstanceResourceIds.size()))); + links.add(new LwM2mLink(rootPath, resourcePath, resourceAttributes.merge( + LwM2mAttributes.create(LwM2mAttributes.DIMENSION, (long) availableInstanceResourceIds.size())))); } else { - links.add(new LwM2mLink(rootPath, new LwM2mPath(objectEnabler.getId(), instanceId, resourceId))); + links.add(new LwM2mLink(rootPath, resourcePath, resourceAttributes)); } // create links for each available instance resource if (!availableInstanceResourceIds.isEmpty() && depth.isResourceInstance()) { for (Integer resourceInstanceId : availableInstanceResourceIds) { - links.add(new LwM2mLink(rootPath, - new LwM2mPath(objectEnabler.getId(), instanceId, resourceId, resourceInstanceId))); + LwM2mPath resourceInstancePath = new LwM2mPath(objectEnabler.getId(), instanceId, resourceId, + resourceInstanceId); + LwM2mAttributeSet resourceInstanceAttributes = getNotificationAttributeFor(server, objectEnabler, + resourceInstancePath); + links.add(new LwM2mLink(rootPath, resourceInstancePath, resourceInstanceAttributes)); } } return links.toArray(new LwM2mLink[links.size()]); } - protected List> getObjectAttributes(ObjectModel objectModel) { + protected LwM2mAttributeSet getNotificationAttributeFor(LwM2mServer server, LwM2mObjectEnabler objectEnabler, + LwM2mPath path) { + NotificationAttributeTree attributeTree = objectEnabler.getAttributesFor(server); + if (attributeTree == null) { + return new LwM2mAttributeSet(); + } + LwM2mAttributeSet attributes = attributeTree.get(path); + if (attributes == null) { + return new LwM2mAttributeSet(); + } + return attributes; + } + + protected LwM2mAttributeSet getObjectAttributes(ObjectModel objectModel) { Version version = getVersion(objectModel); if (version == null) { - return null; + return new LwM2mAttributeSet(); } List> attributes = new ArrayList<>(); attributes.add(LwM2mAttributes.create(LwM2mAttributes.OBJECT_VERSION, version)); - return attributes; + return new LwM2mAttributeSet(attributes); } protected Version getVersion(ObjectModel objectModel) { diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/DefaultLwM2mLinkParser.java b/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/DefaultLwM2mLinkParser.java index f2ed037a48..e575d71e9f 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/DefaultLwM2mLinkParser.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/DefaultLwM2mLinkParser.java @@ -28,6 +28,7 @@ import org.eclipse.leshan.core.link.attributes.Attributes; import org.eclipse.leshan.core.link.attributes.DefaultAttributeParser; import org.eclipse.leshan.core.link.attributes.ResourceTypeAttribute; +import org.eclipse.leshan.core.link.lwm2m.attributes.InvalidAttributesException; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; @@ -103,7 +104,7 @@ public LwM2mLink[] parseLwM2mLinkFromCoreLinkFormat(byte[] bytes, String rootpat // create link and replace it lwm2mLinks[i] = new LwM2mLink(rootpath, new LwM2mPath(path), lwm2mAttrSet); - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException | InvalidAttributesException e) { String strLink = new String(bytes, StandardCharsets.UTF_8); throw new LinkParseException(e, "Unable to parse link %s in %s", links[i], strLink); } @@ -150,7 +151,7 @@ public Link[] parseCoreLinkFormat(byte[] bytes) throws LinkParseException { // create link and replace it links[i] = new MixedLwM2mLink(rootPath, lwm2mPath, attributes); - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException | InvalidAttributesException e) { String strLink = new String(bytes, StandardCharsets.UTF_8); throw new LinkParseException(e, "Unable to parse link %s in %s", links[i], strLink); } diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/InvalidAttributesException.java b/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/InvalidAttributesException.java new file mode 100644 index 0000000000..48671f96dc --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/InvalidAttributesException.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2024 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.core.link.lwm2m.attributes; + +/** + * Exception raised when a collection of Attribute are not valid. + */ +public class InvalidAttributesException extends Exception { + + private static final long serialVersionUID = 1L; + + public InvalidAttributesException(String message) { + super(message); + } + + public InvalidAttributesException(String message, Object... args) { + super(String.format(message, args)); + } + + public InvalidAttributesException(Exception e, String message, Object... args) { + super(String.format(message, args), e); + } + + public InvalidAttributesException(String message, Exception e) { + super(message, e); + } +} diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/LwM2mAttributeModel.java b/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/LwM2mAttributeModel.java index 5b3e9d9f4f..7565fab0a8 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/LwM2mAttributeModel.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/LwM2mAttributeModel.java @@ -18,7 +18,7 @@ import java.util.Set; import org.eclipse.leshan.core.link.attributes.AttributeModel; -import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.node.LwM2mPath; /** @@ -97,7 +97,7 @@ public boolean canBeAttachedTo(Attachment attachement) { * @param model the LWM2M model used, if this model is null checks which need model will be ignored. * @return null is the attribute can be applied to the LWM2M node identified by the given path. */ - public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { + public String getApplicabilityError(LwM2mPath path, ObjectModel model) { if (!canBeAttachedTo(Attachment.fromPath(path))) { return String.format("%s attribute is only applicable to %s, and so can not be attached to %s", getName(), getAttachment(), path); diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/LwM2mAttributes.java b/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/LwM2mAttributes.java index 6e1b1d00b6..e113fcad1d 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/LwM2mAttributes.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/LwM2mAttributes.java @@ -23,7 +23,7 @@ import org.eclipse.leshan.core.LwM2m.LwM2mVersion; import org.eclipse.leshan.core.LwM2m.Version; import org.eclipse.leshan.core.LwM2mId; -import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mPath; @@ -44,14 +44,14 @@ public String getInvalidValueCause(Long value) { }; @Override - public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { + public String getApplicabilityError(LwM2mPath path, ObjectModel model) { String error = super.getApplicabilityError(path, model); if (error != null) return error; if (model != null) { // here the path should be a resource path one. - ResourceModel resourceModel = model.getResourceModel(path.getObjectId(), path.getResourceId()); + ResourceModel resourceModel = model.resources.get(path.getResourceId()); if (resourceModel != null && !resourceModel.multiple) { return "'Dimension' attribute is only applicable to multi-Instance resource"; } @@ -74,7 +74,7 @@ public String getInvalidValueCause(Long value) { }; @Override - public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { + public String getApplicabilityError(LwM2mPath path, ObjectModel model) { String error = super.getApplicabilityError(path, model); if (error != null) return error; @@ -101,7 +101,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { AttributeClass.PROPERTIES) { @Override - public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { + public String getApplicabilityError(LwM2mPath path, ObjectModel model) { String error = super.getApplicabilityError(path, model); if (error != null) return error; @@ -126,7 +126,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { AccessMode.RW, // AttributeClass.NOTIFICATION) { @Override - public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { + public String getApplicabilityError(LwM2mPath path, ObjectModel model) { String error = super.getApplicabilityError(path, model); if (error != null) return error; @@ -135,7 +135,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { Integer resourceId = path.getResourceId(); if (resourceId != null) { // if assigned to at least resource level. - ResourceModel resourceModel = model.getResourceModel(path.getObjectId(), path.getResourceId()); + ResourceModel resourceModel = model.resources.get(path.getResourceId()); if (resourceModel != null && !resourceModel.operations.isReadable()) { return "'pmin' attribute can not be applied to not readable resource"; } @@ -153,7 +153,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { AccessMode.RW, // AttributeClass.NOTIFICATION) { @Override - public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { + public String getApplicabilityError(LwM2mPath path, ObjectModel model) { String error = super.getApplicabilityError(path, model); if (error != null) return error; @@ -162,7 +162,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { Integer resourceId = path.getResourceId(); if (resourceId != null) { // if assigned to at least resource level. - ResourceModel resourceModel = model.getResourceModel(path.getObjectId(), path.getResourceId()); + ResourceModel resourceModel = model.resources.get(path.getResourceId()); if (resourceModel != null && !resourceModel.operations.isReadable()) { return "'pmax' attribute can not be applied to not readable resource"; } @@ -180,7 +180,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { AccessMode.RW, // AttributeClass.NOTIFICATION) { @Override - public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { + public String getApplicabilityError(LwM2mPath path, ObjectModel model) { String error = super.getApplicabilityError(path, model); if (error != null) return error; @@ -189,7 +189,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { Integer resourceId = path.getResourceId(); if (resourceId != null) { // if assigned to at least resource level. - ResourceModel resourceModel = model.getResourceModel(path.getObjectId(), path.getResourceId()); + ResourceModel resourceModel = model.resources.get(path.getResourceId()); if (resourceModel != null) { if (!resourceModel.operations.isReadable()) { return "'gt' attribute is can not be applied to not readable resource"; @@ -212,7 +212,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { AccessMode.RW, // AttributeClass.NOTIFICATION) { @Override - public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { + public String getApplicabilityError(LwM2mPath path, ObjectModel model) { String error = super.getApplicabilityError(path, model); if (error != null) return error; @@ -221,7 +221,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { Integer resourceId = path.getResourceId(); if (resourceId != null) { // if assigned to at least resource level. - ResourceModel resourceModel = model.getResourceModel(path.getObjectId(), path.getResourceId()); + ResourceModel resourceModel = model.resources.get(path.getResourceId()); if (resourceModel != null) { if (!resourceModel.operations.isReadable()) { return "'lt' attribute is can not be applied to not readable resource"; @@ -242,7 +242,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { AccessMode.RW, // AttributeClass.NOTIFICATION) { @Override - public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { + public String getApplicabilityError(LwM2mPath path, ObjectModel model) { String error = super.getApplicabilityError(path, model); if (error != null) return error; @@ -251,7 +251,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { Integer resourceId = path.getResourceId(); if (resourceId != null) { // if assigned to at least resource level. - ResourceModel resourceModel = model.getResourceModel(path.getObjectId(), path.getResourceId()); + ResourceModel resourceModel = model.resources.get(path.getResourceId()); if (resourceModel != null) { if (!resourceModel.operations.isReadable()) { return "'st' attribute is can not be applied to not readable resource"; @@ -274,7 +274,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { AccessMode.RW, // AttributeClass.NOTIFICATION) { @Override - public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { + public String getApplicabilityError(LwM2mPath path, ObjectModel model) { String error = super.getApplicabilityError(path, model); if (error != null) return error; @@ -283,7 +283,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { Integer resourceId = path.getResourceId(); if (resourceId != null) { // if assigned to at least resource level. - ResourceModel resourceModel = model.getResourceModel(path.getObjectId(), path.getResourceId()); + ResourceModel resourceModel = model.resources.get(path.getResourceId()); if (resourceModel != null && !resourceModel.operations.isReadable()) { return "'epmin' attribute is can not be applied to not readable resource"; } @@ -301,7 +301,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { AccessMode.RW, // AttributeClass.NOTIFICATION) { @Override - public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { + public String getApplicabilityError(LwM2mPath path, ObjectModel model) { String error = super.getApplicabilityError(path, model); if (error != null) return error; @@ -310,7 +310,7 @@ public String getApplicabilityError(LwM2mPath path, LwM2mModel model) { Integer resourceId = path.getResourceId(); if (resourceId != null) { // if assigned to at least resource level. - ResourceModel resourceModel = model.getResourceModel(path.getObjectId(), path.getResourceId()); + ResourceModel resourceModel = model.resources.get(path.getResourceId()); if (resourceModel != null && !resourceModel.operations.isReadable()) { return "'epmax' attribute is can not be applied to not readable resource"; } diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/MixedLwM2mAttributeSet.java b/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/MixedLwM2mAttributeSet.java index 3bdcfed3b2..97305f0570 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/MixedLwM2mAttributeSet.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/MixedLwM2mAttributeSet.java @@ -26,6 +26,7 @@ import org.eclipse.leshan.core.link.attributes.Attribute; import org.eclipse.leshan.core.link.attributes.AttributeSet; +import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.node.LwM2mPath; /** @@ -90,25 +91,65 @@ public void remove() { }; } - public void validate(LwM2mPath path) { - // Can all attributes be assigned to this path - for (LwM2mAttribute attr : getLwM2mAttributes()) { - String errorMessage = attr.getModel().getApplicabilityError(path, null); - if (errorMessage != null) { - throw new IllegalArgumentException(errorMessage); - } + public void validate() throws InvalidAttributesException { + // check some consistency about attribute set + // pmin SHOULD BE LESSER or EQUAL TO pmax + // https://datatracker.ietf.org/doc/html/draft-ietf-core-dynlink-07#section-4.2 + LwM2mAttribute pmin = this.getLwM2mAttribute(LwM2mAttributes.MINIMUM_PERIOD); + LwM2mAttribute pmax = this.getLwM2mAttribute(LwM2mAttributes.MAXIMUM_PERIOD); + if ((pmin != null) && (pmax != null) // + && pmin.hasValue() && pmax.hasValue() // + && !(pmin.getValue() <= pmax.getValue())) { + throw new InvalidAttributesException("Attributes doesn't fulfill '%s'<= '%s' condition", pmin.getName(), + pmax.getName()); } + + // epmin SHOULD BE LESSER or EQUAL TO epmax + // https://datatracker.ietf.org/doc/html/draft-ietf-core-conditional-attributes-06#section-3.2.4 + LwM2mAttribute epmin = this.getLwM2mAttribute(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD); + LwM2mAttribute epmax = this.getLwM2mAttribute(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD); + if ((epmin != null) && (epmax != null) // + && epmin.hasValue() && epmax.hasValue() // + && !(epmin.getValue() <= epmax.getValue())) { + throw new InvalidAttributesException("Attributes doesn't fulfill '%s'<= '%s' condition", epmin.getName(), + epmax.getName()); + } + + // "lt" value < "gt" value MUST BE TRUE + // https://www.openmobilealliance.org/release/LightweightM2M/V1_2_1-20221209-A/HTML-Version/OMA-TS-LightweightM2M_Core-V1_2_1-20221209-A.html#7-3-0-73-Attributes + LwM2mAttribute lt = this.getLwM2mAttribute(LwM2mAttributes.LESSER_THAN); + LwM2mAttribute gt = this.getLwM2mAttribute(LwM2mAttributes.GREATER_THAN); + if ((lt != null) && (gt != null) // + && lt.hasValue() && gt.hasValue() // + && !(lt.getValue() < gt.getValue())) { + throw new InvalidAttributesException("Attributes doesn't fulfill '%s'< '%s' condition", lt.getName(), + gt.getName()); + } + + // ("lt" value + 2*"st" values) <"gt" value MUST BE TRUE + // https://www.openmobilealliance.org/release/LightweightM2M/V1_2_1-20221209-A/HTML-Version/OMA-TS-LightweightM2M_Core-V1_2_1-20221209-A.html#7-3-0-73-Attributes + LwM2mAttribute st = this.getLwM2mAttribute(LwM2mAttributes.STEP); + if ((lt != null) && (gt != null) && (st != null) /// + && lt.hasValue() && gt.hasValue() && st.hasValue() // + && !(lt.getValue() + 2 * st.getValue() < gt.getValue())) { + throw new InvalidAttributesException( + "Attributes doesn't fulfill (\"lt\" value + 2*\"st\" values) <\"gt\") condition"); + } + } + + public void validate(LwM2mPath path) throws InvalidAttributesException { + validate(path, null); } - // TODO not sure we still need this function - public void validate(Attachment attachment) { - // Can all attributes be assigned to this level? + public void validate(LwM2mPath path, ObjectModel objectModel) throws InvalidAttributesException { + // Can all attributes be assigned to this path for (LwM2mAttribute attr : getLwM2mAttributes()) { - if (!attr.canBeAttachedTo(attachment)) { - throw new IllegalArgumentException(String.format("Attribute '%s' cannot be attached to level %s", - attr.getName(), attachment.name())); + String errorMessage = attr.getModel().getApplicabilityError(path, objectModel); + if (errorMessage != null) { + throw new InvalidAttributesException(errorMessage); } } + validate(); } /** @@ -126,7 +167,44 @@ public LwM2mAttributeSet merge(LwM2mAttributeSet attributes) { } if (attributes != null) { for (LwM2mAttribute attr : attributes.getLwM2mAttributes()) { - merged.put(attr.getName(), attr); + if (attr.hasValue()) { + merged.put(attr.getName(), attr); + } else { + merged.remove(attr.getName()); + } + } + } + return new LwM2mAttributeSet(merged.values()); + } + + /** + * Creates a new AttributeSet by merging given attributes. + * + * @param attributes the array that should be merged onto this instance. Attributes in this array will overwrite + * existing attribute values, if present. If this is null, the new attribute set will effectively be a clone + * of the existing one + * @return the merged AttributeSet + */ + public LwM2mAttributeSet merge(LwM2mAttribute... attributes) { + return this.merge(new LwM2mAttributeSet(attributes)); + } + + /** + * Like {@link #merge(LwM2mAttributeSet)} except if a given {@link LwM2mAttribute} has no value, it will remove this + * attribute from final result. + */ + public LwM2mAttributeSet apply(LwM2mAttributeSet attributes) { + Map> merged = new LinkedHashMap<>(); + for (LwM2mAttribute attr : getLwM2mAttributes()) { + merged.put(attr.getName(), attr); + } + if (attributes != null) { + for (LwM2mAttribute attr : attributes.getLwM2mAttributes()) { + if (attr.hasValue()) { + merged.put(attr.getName(), attr); + } else { + merged.remove(attr.getName()); + } } } return new LwM2mAttributeSet(merged.values()); diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/NotificationAttributeTree.java b/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/NotificationAttributeTree.java new file mode 100644 index 0000000000..2053b4a049 --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/link/lwm2m/attributes/NotificationAttributeTree.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2024 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.core.link.lwm2m.attributes; + +import java.util.Set; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; + +import org.eclipse.leshan.core.node.LwM2mPath; + +/** + * A kind of tree structure which stores {@link LwM2mAttributeSet} by {@link LwM2mPath}. + */ +public class NotificationAttributeTree { + + private final ConcurrentNavigableMap internalTree = new ConcurrentSkipListMap<>(); + + public void put(LwM2mPath path, LwM2mAttributeSet newValue) { + if (newValue.isEmpty()) { + internalTree.remove(path); + } else { + internalTree.put(path, newValue); + } + } + + public void remove(LwM2mPath path) { + internalTree.remove(path); + } + + /** + * Remove all attribute for the given {@link LwM2mPath} and all its children. + */ + public void removeAllUnder(LwM2mPath parentPath) { + internalTree.subMap(parentPath, true, parentPath.toMaxDescendant(), true).clear(); + } + + public boolean isEmpty() { + return internalTree.isEmpty(); + } + + public LwM2mAttributeSet get(LwM2mPath path) { + return internalTree.get(path); + } + + /** + * @return all children {@link LwM2mPath} of given parent {@link LwM2mPath} which have attached + * {@link LwM2mAttributeSet} + */ + public Set getChildren(LwM2mPath parentPath) { + return internalTree.subMap(parentPath, false, parentPath.toMaxDescendant(), true).keySet(); + } + + /** + * @return {@link LwM2mAttributeSet} attached to given level merged with value inherited from higher level. + * + * @see LWM2M-v1.2.1@core7.3.2. + * NOTIFICATION Class Attributes + */ + public LwM2mAttributeSet getWithInheritance(LwM2mPath path) { + // For Root Path no need to "flatten" hierarchy + if (path.isRoot()) { + return get(path); + } + + // For not root path + // Create Attribute Set taking inherited value into account. + LwM2mAttributeSet result = get(path); + LwM2mPath parentPath = path.toParenPath(); + while (!parentPath.isRoot()) { + LwM2mAttributeSet parentAttributes = get(parentPath); + if (parentAttributes != null) { + result = parentAttributes.merge(result); + } + parentPath = parentPath.toParenPath(); + } + return result; + } +} diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteAttributesRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteAttributesRequest.java index 64e12205ad..c85aeba96a 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteAttributesRequest.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteAttributesRequest.java @@ -16,9 +16,9 @@ package org.eclipse.leshan.core.request; import org.eclipse.leshan.core.link.lwm2m.attributes.AttributeClass; +import org.eclipse.leshan.core.link.lwm2m.attributes.InvalidAttributesException; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; -import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.request.exception.InvalidRequestException; import org.eclipse.leshan.core.response.WriteAttributesResponse; @@ -84,28 +84,9 @@ private WriteAttributesRequest(LwM2mPath path, LwM2mAttributeSet attributes, Obj } try { attributes.validate(path); - - } catch (IllegalArgumentException e) { + } catch (InvalidAttributesException e) { throw new InvalidRequestException(e, "Some attributes are not valid for the path %s.", path); } - - // check some consistency about attribute set - LwM2mAttribute pmin = attributes.getLwM2mAttribute(LwM2mAttributes.MINIMUM_PERIOD); - LwM2mAttribute pmax = attributes.getLwM2mAttribute(LwM2mAttributes.MAXIMUM_PERIOD); - if ((pmin != null) && (pmax != null) && pmin.hasValue() && pmax.hasValue() - && pmin.getValue() > pmax.getValue()) { - throw new InvalidRequestException("Cannot write attributes where '%s' > '%s'", pmin.getName(), - pmax.getName()); - } - - LwM2mAttribute epmin = attributes.getLwM2mAttribute(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD); - LwM2mAttribute epmax = attributes.getLwM2mAttribute(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD); - if ((epmin != null) && (epmax != null) && epmin.hasValue() && epmax.hasValue() - && epmin.getValue() > epmax.getValue()) { - throw new InvalidRequestException("Cannot write attributes where '%s' > '%s'", epmin.getName(), - epmax.getName()); - } - } @Override diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/link/attributes/AttributeSetTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/link/attributes/AttributeSetTest.java index 25348e0d88..ccfb2a7114 100644 --- a/leshan-core/src/test/java/org/eclipse/leshan/core/link/attributes/AttributeSetTest.java +++ b/leshan-core/src/test/java/org/eclipse/leshan/core/link/attributes/AttributeSetTest.java @@ -23,11 +23,13 @@ import java.util.Map; import org.eclipse.leshan.core.LwM2m.Version; -import org.eclipse.leshan.core.link.lwm2m.attributes.Attachment; import org.eclipse.leshan.core.link.lwm2m.attributes.DefaultLwM2mAttributeParser; +import org.eclipse.leshan.core.link.lwm2m.attributes.InvalidAttributesException; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeParser; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.node.InvalidLwM2mPathException; +import org.eclipse.leshan.core.node.LwM2mPath; import org.junit.jupiter.api.Test; public class AttributeSetTest { @@ -134,24 +136,24 @@ public void should_throw_on_duplicates() { } @Test - public void should_validate_assignation() { + public void should_validate_assignation() throws InvalidLwM2mPathException, InvalidAttributesException { LwM2mAttributeSet sut = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 5L), LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 60L)); Collection attributes = sut.asCollection(); assertEquals(2, attributes.size()); - sut.validate(Attachment.RESOURCE); + sut.validate(new LwM2mPath("/3/0/9")); } @Test public void should_throw_on_invalid_assignation_level() { - assertThrowsExactly(IllegalArgumentException.class, () -> { + assertThrowsExactly(InvalidAttributesException.class, () -> { LwM2mAttributeSet sut = new LwM2mAttributeSet( LwM2mAttributes.create(LwM2mAttributes.OBJECT_VERSION, new Version("1.1")), LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 5L), LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 60L)); // OBJECT_VERSION cannot be assigned on resource level - sut.validate(Attachment.RESOURCE); + sut.validate(new LwM2mPath("/3/0/9")); }); } } diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/link/attributes/AttributeTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/link/attributes/AttributeTest.java index d4cc9c93bc..636671028f 100644 --- a/leshan-core/src/test/java/org/eclipse/leshan/core/link/attributes/AttributeTest.java +++ b/leshan-core/src/test/java/org/eclipse/leshan/core/link/attributes/AttributeTest.java @@ -17,6 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; import static org.junit.jupiter.api.Assertions.assertTrue; import org.eclipse.leshan.core.LwM2m.Version; @@ -36,4 +37,50 @@ public void should_pick_correct_model() { assertTrue(verAttribute.canBeAttachedTo(Attachment.OBJECT)); assertFalse(verAttribute.isWritable()); } + + @Test + public void should_throw_on_pmin_lesser_than_zero() { + // The minimum period MUST be greater than zero + // https://datatracker.ietf.org/doc/html/draft-ietf-core-dynlink-07#section-4.1 + + assertThrowsExactly(IllegalArgumentException.class, () -> { + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, -1L); + }); + } + + @Test + public void should_throw_on_pmax_lesser_than_zero() { + // The maximum period MUST be greater than zero + // https://datatracker.ietf.org/doc/html/draft-ietf-core-dynlink-07#section-4.2 + assertThrowsExactly(IllegalArgumentException.class, () -> { + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, -1L); + }); + } + + @Test + public void should_throw_on_epmin_lesser_than_zero() { + // The Minimum Evaluation Period MUST be greater than zero + // https://datatracker.ietf.org/doc/html/draft-ietf-core-conditional-attributes-06#section-3.2.3 + assertThrowsExactly(IllegalArgumentException.class, () -> { + LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD, -1L); + }); + } + + @Test + public void should_throw_on_epmax_lesser_than_zero() { + // The Maximum Evaluation Period MUST be greater than zero + // https://datatracker.ietf.org/doc/html/draft-ietf-core-conditional-attributes-06#section-3.2.4 + assertThrowsExactly(IllegalArgumentException.class, () -> { + LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD, -1L); + }); + } + + @Test + public void should_throw_on_step_lesser_than_zero() { + // The Maximum Evaluation Period MUST be greater than zero + // https://datatracker.ietf.org/doc/html/draft-ietf-core-conditional-attributes-06#section-3.2.4 + assertThrowsExactly(IllegalArgumentException.class, () -> { + LwM2mAttributes.create(LwM2mAttributes.STEP, -1D); + }); + } } diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/request/WriteAttributesRequestTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/request/WriteAttributesRequestTest.java index a773264a78..73f3270d8c 100644 --- a/leshan-core/src/test/java/org/eclipse/leshan/core/request/WriteAttributesRequestTest.java +++ b/leshan-core/src/test/java/org/eclipse/leshan/core/request/WriteAttributesRequestTest.java @@ -24,9 +24,13 @@ public class WriteAttributesRequestTest { - @Test() - public void should_throw_on_invalid_pmin_pmax() { - LwM2mAttributeSet sut = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 50L), + @Test + public void should_throw_on_pmin_greater_than_pmax() { + // The maximum period MUST be greater than the minimum period parameter + // https://datatracker.ietf.org/doc/html/draft-ietf-core-dynlink-07#section-4.2 + + LwM2mAttributeSet sut = new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 50L), // LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 49L)); // pmin cannot be greater then pmax @@ -36,10 +40,43 @@ public void should_throw_on_invalid_pmin_pmax() { } @Test - public void should_throw_on_invalid_epmin_epmax() { - LwM2mAttributeSet sut = new LwM2mAttributeSet( - LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD, 50L), - LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD, 49L)); + public void should_throw_on_epmin_greater_than_pmax() { + // The Maximum Evaluation Period MUST be greater than the Minimum Evaluation Period parameter + // https://datatracker.ietf.org/doc/html/draft-ietf-core-conditional-attributes-06#section-3.2.4 + + LwM2mAttributeSet sut = new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 50L), // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 49L)); + + // pmin cannot be greater then pmax + assertThrowsExactly(InvalidRequestException.class, () -> { + new WriteAttributesRequest(3, 0, 9, sut); + }); + } + + @Test + public void should_throw_on_lt_greater_than_gt() { + // "lt" value < "gt" value + // https://www.openmobilealliance.org/release/LightweightM2M/V1_2_1-20221209-A/HTML-Version/OMA-TS-LightweightM2M_Core-V1_2_1-20221209-A.html#7-3-0-73-Attributes + + LwM2mAttributeSet sut = new LwM2mAttributeSet(// + LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 50d), // + LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 49d)); + + // pmin cannot be greater then pmax + assertThrowsExactly(InvalidRequestException.class, () -> { + new WriteAttributesRequest(3, 0, 9, sut); + }); + } + + @Test + public void should_throw_on_gt_equals_lt() { + // "lt" value < "gt" value + // https://www.openmobilealliance.org/release/LightweightM2M/V1_2_1-20221209-A/HTML-Version/OMA-TS-LightweightM2M_Core-V1_2_1-20221209-A.html#7-3-0-73-Attributes + + LwM2mAttributeSet sut = new LwM2mAttributeSet(// + LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 50d), // + LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 50d)); // pmin cannot be greater then pmax assertThrowsExactly(InvalidRequestException.class, () -> { @@ -47,4 +84,19 @@ public void should_throw_on_invalid_epmin_epmax() { }); } + @Test + public void should_throw_on_invalid_lt_gt_st_combination() { + // ("lt" value + 2*"st" values) <"gt" value + // https://www.openmobilealliance.org/release/LightweightM2M/V1_2_1-20221209-A/HTML-Version/OMA-TS-LightweightM2M_Core-V1_2_1-20221209-A.html#7-3-0-73-Attributes + + LwM2mAttributeSet sut = new LwM2mAttributeSet(// + LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 10d), // + LwM2mAttributes.create(LwM2mAttributes.STEP, 2d), // + LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 12d)); + + // pmin cannot be greater then pmax + assertThrowsExactly(InvalidRequestException.class, () -> { + new WriteAttributesRequest(3, 0, 9, sut); + }); + } } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeBootstrapTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeBootstrapTest.java new file mode 100644 index 0000000000..5a650b9825 --- /dev/null +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeBootstrapTest.java @@ -0,0 +1,204 @@ +/******************************************************************************* + * Copyright (c) 2024 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 + * Michał Wadowski (Orange) - Improved compliance with rfc6690 + *******************************************************************************/ +package org.eclipse.leshan.integration.tests.attributes; + +import static org.eclipse.leshan.core.util.TestLwM2mId.MULTIPLE_INTEGER_VALUE; +import static org.eclipse.leshan.core.util.TestLwM2mId.TEST_OBJECT; +import static org.eclipse.leshan.integration.tests.BootstrapConfigTestBuilder.givenBootstrapConfig; +import static org.eclipse.leshan.integration.tests.util.LeshanTestBootstrapServerBuilder.givenBootstrapServerUsing; +import static org.eclipse.leshan.integration.tests.util.LeshanTestClientBuilder.givenClientUsing; +import static org.eclipse.leshan.integration.tests.util.LeshanTestServerBuilder.givenServerUsing; +import static org.eclipse.leshan.integration.tests.util.assertion.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.eclipse.leshan.core.endpoint.Protocol; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.node.InvalidLwM2mPathException; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.request.WriteAttributesRequest; +import org.eclipse.leshan.core.response.ObserveResponse; +import org.eclipse.leshan.core.response.WriteAttributesResponse; +import org.eclipse.leshan.integration.tests.util.LeshanTestBootstrapServer; +import org.eclipse.leshan.integration.tests.util.LeshanTestBootstrapServerBuilder; +import org.eclipse.leshan.integration.tests.util.LeshanTestClient; +import org.eclipse.leshan.integration.tests.util.LeshanTestClientBuilder; +import org.eclipse.leshan.integration.tests.util.LeshanTestServer; +import org.eclipse.leshan.integration.tests.util.LeshanTestServerBuilder; +import org.eclipse.leshan.integration.tests.util.junit5.extensions.BeforeEachParameterizedResolver; +import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException; +import org.eclipse.leshan.server.registration.Registration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(BeforeEachParameterizedResolver.class) +public class WriteAttributeBootstrapTest { + + /*---------------------------------/ + * Parameterized Tests + * -------------------------------*/ + @ParameterizedTest(name = "{0} - Client using {1} - Server using {2}- BS Server using {3}") + @MethodSource("transports") + @Retention(RetentionPolicy.RUNTIME) + private @interface TestAllTransportLayer { + } + + static Stream transports() { + return Stream.of(// + // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - BS Server Endpoint Provider + arguments(Protocol.COAP, "Californium", "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap", "Californium"), // + arguments(Protocol.COAP, "java-coap", "Californium", "Californium"), // + arguments(Protocol.COAP, "java-coap", "java-coap", "Californium")); + } + + /*---------------------------------/ + * Set-up and Tear-down Tests + * -------------------------------*/ + LeshanTestBootstrapServerBuilder givenBootstrapServer; + LeshanTestBootstrapServer bootstrapServer; + LeshanTestServerBuilder givenServer; + LeshanTestServer server; + LeshanTestClientBuilder givenClient; + LeshanTestClient client; + + @BeforeEach + public void start(Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider, + String givenBootstrapServerEndpointProvider) { + givenServer = givenServerUsing(givenProtocol).with(givenServerEndpointProvider); + givenBootstrapServer = givenBootstrapServerUsing(givenProtocol).with(givenBootstrapServerEndpointProvider); + givenClient = givenClientUsing(givenProtocol).with(givenClientEndpointProvider); + } + + @AfterEach + public void stop() throws InterruptedException { + if (client != null) + client.destroy(false); + if (server != null) + server.destroy(); + if (bootstrapServer != null) + bootstrapServer.destroy(); + } + + /*---------------------------------/ + * Tests + * -------------------------------*/ + @TestAllTransportLayer + public void write_attribute_then_observe_object_then_bootstrap_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider, + String givenBootstrapServerEndpointProvider) + throws InterruptedException, InvalidLwM2mPathException, InvalidConfigurationException { + + write_attribute_then_observe_then_bootstrap_then_check_no_more_notification_data(givenProtocol, + new LwM2mPath(TEST_OBJECT)); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_object_instance_then_bootstrap_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider, + String givenBootstrapServerEndpointProvider) + throws InterruptedException, InvalidLwM2mPathException, InvalidConfigurationException { + + write_attribute_then_observe_then_bootstrap_then_check_no_more_notification_data(givenProtocol, + new LwM2mPath(TEST_OBJECT, 0)); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_resource_then_bootstrap_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider, + String givenBootstrapServerEndpointProvider) + throws InterruptedException, InvalidLwM2mPathException, InvalidConfigurationException { + + write_attribute_then_observe_then_bootstrap_then_check_no_more_notification_data(givenProtocol, + new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE)); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_resource_instance_then_bootstrap_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider, + String givenBootstrapServerEndpointProvider) + throws InterruptedException, InvalidLwM2mPathException, InvalidConfigurationException { + + write_attribute_then_observe_then_bootstrap_then_check_no_more_notification_data(givenProtocol, + new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE, 0)); + } + + public void write_attribute_then_observe_then_bootstrap_then_check_no_more_notification_data(Protocol givenProtocol, + LwM2mPath targetedPath) throws InterruptedException, InvalidConfigurationException { + + // Create DM Server without security & start it + server = givenServer.build(); + server.start(); + + // Create and start bootstrap server + bootstrapServer = givenBootstrapServer.build(); + bootstrapServer.start(); + + // Create Client and check it is not already registered + client = givenClient.connectingTo(bootstrapServer).build(); + assertThat(client).isNotRegisteredAt(server); + + // Add config for this client + bootstrapServer.getConfigStore().add(client.getEndpointName(), // + givenBootstrapConfig() // + .adding(givenProtocol, bootstrapServer) // + .adding(givenProtocol, server) // + .build()); + + // Start it and wait for registration + client.start(); + client.waitForBootstrapSuccess(bootstrapServer, 1, TimeUnit.SECONDS); + server.waitForNewRegistrationOf(client); + Registration currentRegistration = server.getRegistrationFor(client); + + // check the client is registered + assertThat(client).isRegisteredAt(server); + + // Add Attribute pmin=1seconds to targeted node + WriteAttributesResponse writeAttributeResponse = server.send(currentRegistration, new WriteAttributesRequest( + targetedPath, new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 1l)))); + assertThat(writeAttributeResponse).isSuccess(); + + // Check there is no notification data + assertThat(client).hasNoNotificationData(); + + // Observe targeted node + ObserveResponse response = server.send(currentRegistration, new ObserveRequest(targetedPath)); + assertThat(response).isSuccess(); + + // Check that we have new NotificationData + Thread.sleep(100); // wait to be sure client handle observation : TODO We should do better. + assertThat(client).hasNotificationData(); + + // Force Bootstrap + client.triggerClientInitiatedBootstrap(false); + + // then assert the is no more notification data + client.waitForBootstrapSuccess(bootstrapServer, 1, TimeUnit.SECONDS); + assertThat(client).hasNoNotificationData(); + } + +} diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeDiscoverTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeDiscoverTest.java new file mode 100644 index 0000000000..72c113470c --- /dev/null +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeDiscoverTest.java @@ -0,0 +1,427 @@ +/******************************************************************************* + * Copyright (c) 2024 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.integration.tests.attributes; + +import static org.eclipse.leshan.core.ResponseCode.CHANGED; +import static org.eclipse.leshan.core.ResponseCode.CONTENT; +import static org.eclipse.leshan.integration.tests.util.LeshanTestClientBuilder.givenClientUsing; +import static org.eclipse.leshan.integration.tests.util.LeshanTestServerBuilder.givenServerUsing; +import static org.eclipse.leshan.integration.tests.util.assertion.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.leshan.client.object.Device; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.LwM2mId; +import org.eclipse.leshan.core.endpoint.Protocol; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.ResourceModel.Type; +import org.eclipse.leshan.core.request.DiscoverRequest; +import org.eclipse.leshan.core.request.WriteAttributesRequest; +import org.eclipse.leshan.core.response.DiscoverResponse; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.core.response.WriteAttributesResponse; +import org.eclipse.leshan.integration.tests.util.LeshanTestClient; +import org.eclipse.leshan.integration.tests.util.LeshanTestServer; +import org.eclipse.leshan.integration.tests.util.junit5.extensions.BeforeEachParameterizedResolver; +import org.eclipse.leshan.server.registration.Registration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(BeforeEachParameterizedResolver.class) +public class WriteAttributeDiscoverTest { + + /*---------------------------------/ + * Parameterized Tests + * -------------------------------*/ + @ParameterizedTest(name = "{0} - Client using {1} - Server using {2}") + @MethodSource("transports") + @Retention(RetentionPolicy.RUNTIME) + private @interface TestAllTransportLayer { + } + + static Stream transports() { + return Stream.of(// + // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap"), // + arguments(Protocol.COAP, "java-coap", "Californium"), // + arguments(Protocol.COAP, "java-coap", "java-coap")); + } + + /*---------------------------------/ + * Set-up and Tear-down Tests + * -------------------------------*/ + + LeshanTestServer server; + LeshanTestClient client; + Registration currentRegistration; + + private static class WriteAttributeTestDevice extends Device { + + private static final List supportedResources = Arrays.asList(0, 7, 9, 16); + HashMap powerSourceVoltage; + + public WriteAttributeTestDevice() { + super("test_manufacturer", "model_number", "serial"); + powerSourceVoltage = new HashMap<>(); + powerSourceVoltage.put(0, 55l); + powerSourceVoltage.put(1, 65l); + } + + @Override + public List getAvailableResourceIds(ObjectModel model) { + return supportedResources; + } + + @Override + public ReadResponse read(LwM2mServer server, int resourceid) { + + switch (resourceid) { + case 7: // error codes + return ReadResponse.success(resourceid, powerSourceVoltage, Type.INTEGER); + default: + return super.read(server, resourceid); + } + } + } + + @BeforeEach + public void start(Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) { + server = givenServerUsing(givenProtocol).with(givenServerEndpointProvider).build(); + server.start(); + client = givenClientUsing(givenProtocol) + // we create client with a custom device to have pretty small discover response (making test readable) + .withInstancesForObject(LwM2mId.DEVICE, new WriteAttributeTestDevice()) + .with(givenClientEndpointProvider).connectingTo(server).build(); + client.start(); + server.waitForNewRegistrationOf(client); + client.waitForRegistrationTo(server); + + currentRegistration = server.getRegistrationFor(client); + + } + + @AfterEach + public void stop() throws InterruptedException { + if (client != null) + client.destroy(false); + if (server != null) + server.destroy(); + } + + /*---------------------------------/ + * Tests + * -------------------------------*/ + @TestAllTransportLayer + public void write_attribute_on_object_then_discover(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + // Check there is no attribute already set + DiscoverResponse response = server.send(currentRegistration, new DiscoverRequest(LwM2mId.DEVICE)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(",,,;dim=2,,"); + + // Write some attributes + WriteAttributesResponse writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(LwM2mId.DEVICE, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 100l), // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 200l) // + ))); + assertThat(writeAttributesResponse) // + .hasCode(CHANGED) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // Check those attributes are now visible when discovering at object level + response = server.send(currentRegistration, new DiscoverRequest(LwM2mId.DEVICE)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(";pmin=100;pmax=200,,,;dim=2,,"); + + // override attribute + writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(LwM2mId.DEVICE, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 150l) // + ))); + assertThat(writeAttributesResponse) // + .hasCode(CHANGED) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // Check it + response = server.send(currentRegistration, new DiscoverRequest(LwM2mId.DEVICE)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(";pmin=150;pmax=200,,,;dim=2,,"); + + // remove attribute + writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(LwM2mId.DEVICE, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD) // + ))); + assertThat(writeAttributesResponse) // + .hasCode(CHANGED) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // Check it + response = server.send(currentRegistration, new DiscoverRequest(LwM2mId.DEVICE)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(";pmin=150,,,;dim=2,,"); + + // add + override attribute + writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(LwM2mId.DEVICE, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 300l), // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 600l) // + ))); + assertThat(writeAttributesResponse) // + .hasCode(CHANGED) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // Check it + response = server.send(currentRegistration, new DiscoverRequest(LwM2mId.DEVICE)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(";pmin=300;pmax=600,,,;dim=2,,"); + } + + @TestAllTransportLayer + public void write_attribute_on_object_instance_then_discover(Protocol givenProtocol, + String givenClientEndpointProvider, String givenServerEndpointProvider) throws InterruptedException { + + // Check there is no attribute already set + DiscoverResponse response = server.send(currentRegistration, new DiscoverRequest(LwM2mId.DEVICE, 0)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(",,;dim=2,,"); + + // Write some attributes + WriteAttributesResponse writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(LwM2mId.DEVICE, 0, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 100l), // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 200l) // + ))); + assertThat(writeAttributesResponse) // + .hasCode(CHANGED) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // Check those attributes are now visible when discovering at object level + response = server.send(currentRegistration, new DiscoverRequest(LwM2mId.DEVICE)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(",;pmin=100;pmax=200,,;dim=2,,"); + + // Check those attributes are now visible when discovering at object instance level + response = server.send(currentRegistration, new DiscoverRequest(LwM2mId.DEVICE, 0)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(";pmin=100;pmax=200,,;dim=2,,"); + + } + + @TestAllTransportLayer + public void write_attribute_on_single_resource_then_discover(Protocol givenProtocol, + String givenClientEndpointProvider, String givenServerEndpointProvider) throws InterruptedException { + + // Check there is no attribute already set + DiscoverResponse response = server.send(currentRegistration, new DiscoverRequest(LwM2mId.DEVICE, 0, 9)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(""); + + // Write some attributes + WriteAttributesResponse writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(3, 0, 9, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 100l), // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 200l), // + LwM2mAttributes.create(LwM2mAttributes.STEP, 1d), // + LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 20d), // + LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 50d) // + ))); + assertThat(writeAttributesResponse) // + .hasCode(CHANGED) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // Check those attributes are now visible when discovering at object level + response = server.send(currentRegistration, new DiscoverRequest(3)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike( + ",,,;dim=2,;pmin=100;pmax=200;st=1;lt=20;gt=50,"); + + // Check those attributes are now visible when discovering at object instance level + response = server.send(currentRegistration, new DiscoverRequest(3, 0)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike( + ",,;dim=2,;pmin=100;pmax=200;st=1;lt=20;gt=50,"); + + // Check those attributes are now visible when discovering at resource level + response = server.send(currentRegistration, new DiscoverRequest(3, 0, 9)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(";pmin=100;pmax=200;st=1;lt=20;gt=50"); + } + + @TestAllTransportLayer + public void write_attribute_on_resource_instance_then_discover(Protocol givenProtocol, + String givenClientEndpointProvider, String givenServerEndpointProvider) throws InterruptedException { + + // Check there is no attribute already set + DiscoverResponse response = server.send(currentRegistration, new DiscoverRequest(LwM2mId.DEVICE, 0, 7)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(";dim=2,,"); + + // Write some attributes + WriteAttributesResponse writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(3, 0, 7, 0, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 100l), // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 200l), // + LwM2mAttributes.create(LwM2mAttributes.STEP, 1d), // + LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 20d), // + LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 50d) // + ))); + assertThat(writeAttributesResponse) // + .hasCode(CHANGED) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // Check those attributes are now visible when discovering at object level + response = server.send(currentRegistration, new DiscoverRequest(3)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(",,,;dim=2,,"); + + // Check those attributes are now visible when discovering at object instance level + response = server.send(currentRegistration, new DiscoverRequest(3, 0)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(",,;dim=2,,"); + + // Check those attributes are now visible when discovering at resource level + response = server.send(currentRegistration, new DiscoverRequest(3, 0, 7)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(";dim=2,;pmin=100;pmax=200;st=1;lt=20;gt=50,"); + } + + @TestAllTransportLayer + public void write_attribute_at_all_level_then_discover(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + // Check there is no attribute already set + DiscoverResponse response = server.send(currentRegistration, new DiscoverRequest(LwM2mId.DEVICE)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike(",,,;dim=2,,"); + + // Write some attributes at object level + WriteAttributesResponse writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(LwM2mId.DEVICE, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 1000l), // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 2000l) // + ))); + assertThat(writeAttributesResponse) // + .hasCode(CHANGED) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + // Write some attributes at object install level + writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(LwM2mId.DEVICE, 0, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 100l), // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 200l) // + ))); + assertThat(writeAttributesResponse) // + .hasCode(CHANGED) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + // Write some attributes at resource level + writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(3, 0, 7, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 10l), // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 20l), // + LwM2mAttributes.create(LwM2mAttributes.STEP, 10d), // + LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 200d), // + LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 500d) // + ))); + assertThat(writeAttributesResponse) // + .hasCode(CHANGED) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + // Write some attributes at resource instance level + writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(3, 0, 7, 0, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 1l), // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 2l), // + LwM2mAttributes.create(LwM2mAttributes.STEP, 1d), // + LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 20d), // + LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 50d) // + ))); + assertThat(writeAttributesResponse) // + .hasCode(CHANGED) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // Check those attributes are now visible when discovering at object level + response = server.send(currentRegistration, new DiscoverRequest(3)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike( + ";pmin=1000;pmax=2000,;pmin=100;pmax=200,,;dim=2;pmin=10;pmax=20;st=10;lt=200;gt=500,,"); + + // Check those attributes are now visible when discovering at object instance level + response = server.send(currentRegistration, new DiscoverRequest(3, 0)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike( + ";pmin=100;pmax=200,,;dim=2;pmin=10;pmax=20;st=10;lt=200;gt=500,,"); + + // Check those attributes are now visible when discovering at resource level + response = server.send(currentRegistration, new DiscoverRequest(3, 0, 7)); + assertThat(response) // + .hasCode(CONTENT) // + .hasValidUnderlyingResponseFor(givenServerEndpointProvider) // + .hasObjectLinksLike( + ";dim=2;pmin=10;pmax=20;st=10;lt=200;gt=500,;pmin=1;pmax=2;st=1;lt=20;gt=50,"); + } +} diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeFailedTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeFailedTest.java new file mode 100644 index 0000000000..81b8aa16d7 --- /dev/null +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeFailedTest.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * Copyright (c) 2024 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.integration.tests.attributes; + +import static org.eclipse.leshan.integration.tests.util.LeshanTestClientBuilder.givenClientUsing; +import static org.eclipse.leshan.integration.tests.util.LeshanTestServerBuilder.givenServerUsing; +import static org.eclipse.leshan.integration.tests.util.assertion.Assertions.assertThat; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.stream.Stream; + +import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.endpoint.Protocol; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.request.WriteAttributesRequest; +import org.eclipse.leshan.core.response.WriteAttributesResponse; +import org.eclipse.leshan.integration.tests.util.LeshanTestClient; +import org.eclipse.leshan.integration.tests.util.LeshanTestServer; +import org.eclipse.leshan.integration.tests.util.junit5.extensions.ArgumentsUtil; +import org.eclipse.leshan.integration.tests.util.junit5.extensions.BeforeEachParameterizedResolver; +import org.eclipse.leshan.server.registration.Registration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(BeforeEachParameterizedResolver.class) +public class WriteAttributeFailedTest { + + /*---------------------------------/ + * Parameterized Tests + * -------------------------------*/ + @ParameterizedTest(name = "{0} - Client using {1} - Server using {2} - {3} - {4},{5}") + @MethodSource("transports") + @Retention(RetentionPolicy.RUNTIME) + private @interface TestAllCases { + } + + static Stream transports() { + + Object[][] transports = new Object[][] { + // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider + { Protocol.COAP, "Californium", "Californium" }, // + { Protocol.COAP, "Californium", "java-coap" }, // + { Protocol.COAP, "java-coap", "Californium" }, // + { Protocol.COAP, "java-coap", "java-coap" } }; + + Object[][] testCases = new Object[][] { // + // targeted path - initial state - invalid attributes to write + + { // test pmin > pmax + "/3", // + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 300l)), // + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 200l)) // + }, // + { // test epmin > epmax + "/3", // + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD, 300l)), // + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD, 200l)) // + }, // + { // test gt < lt + "/3/0/9", // + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 100d)), // + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 200d)) // + }, // + { // test gt == lt + "/3/0/9", // + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 200d)), // + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 200d)) // + }, // + { // test lt - 2*st > gt + "/3/0/9", // + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 11d)), // + new LwM2mAttributeSet(LwM2mAttributes.create( // + LwM2mAttributes.LESSER_THAN, 10d), // + LwM2mAttributes.create(LwM2mAttributes.STEP, 2d)) // + }, // + { // test lt - 2*st == gt + "/3/0/9", // + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 14d)), // + new LwM2mAttributeSet(LwM2mAttributes.create( // + LwM2mAttributes.LESSER_THAN, 10d), // + LwM2mAttributes.create(LwM2mAttributes.STEP, 2d)) // + } }; + + // for each transport, create 1 test by format. + return Stream.of(ArgumentsUtil.combine(transports, testCases)); + } + + /*---------------------------------/ + * Set-up and Tear-down Tests + * -------------------------------*/ + + LeshanTestServer server; + LeshanTestClient client; + Registration currentRegistration; + + @BeforeEach + public void start(Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider, + String targetedPath, LwM2mAttributeSet initialState, LwM2mAttributeSet invalidAttributes) { + server = givenServerUsing(givenProtocol).with(givenServerEndpointProvider).build(); + server.start(); + client = givenClientUsing(givenProtocol).with(givenClientEndpointProvider).connectingTo(server).build(); + client.start(); + server.waitForNewRegistrationOf(client); + client.waitForRegistrationTo(server); + + currentRegistration = server.getRegistrationFor(client); + + } + + @AfterEach + public void stop() throws InterruptedException { + if (client != null) + client.destroy(false); + if (server != null) + server.destroy(); + } + + /*---------------------------------/ + * Tests + * -------------------------------*/ + @TestAllCases + public void test_failing(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider, String targetedPath, LwM2mAttributeSet initialState, + LwM2mAttributeSet invalidAttributes) throws InterruptedException { + + LwM2mPath path = new LwM2mPath(targetedPath); + + // Set initial state if needed + if (initialState != null) { + WriteAttributesResponse writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(path, initialState)); + assertThat(writeAttributesResponse).isSuccess(); + } + + // Write failing attributes + WriteAttributesResponse writeAttributesResponse = server.send(currentRegistration, + new WriteAttributesRequest(path, invalidAttributes)); + assertThat(writeAttributesResponse).hasCode(ResponseCode.BAD_REQUEST) + .hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + } +} diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeHouseKeepingTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeHouseKeepingTest.java new file mode 100644 index 0000000000..4efabea271 --- /dev/null +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeHouseKeepingTest.java @@ -0,0 +1,382 @@ +/******************************************************************************* + * Copyright (c) 2024 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.integration.tests.attributes; + +import static org.eclipse.leshan.core.util.TestLwM2mId.MULTIPLE_INTEGER_VALUE; +import static org.eclipse.leshan.core.util.TestLwM2mId.TEST_OBJECT; +import static org.eclipse.leshan.integration.tests.util.LeshanTestClientBuilder.givenClientUsing; +import static org.eclipse.leshan.integration.tests.util.assertion.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.endpoint.Protocol; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.model.ResourceModel.Type; +import org.eclipse.leshan.core.node.LwM2mMultipleResource; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.LwM2mResourceInstance; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.request.CancelObservationRequest; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.DeleteRequest; +import org.eclipse.leshan.core.request.DownlinkRequest; +import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.request.WriteAttributesRequest; +import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.request.WriteRequest.Mode; +import org.eclipse.leshan.core.response.CancelObservationResponse; +import org.eclipse.leshan.core.response.LwM2mResponse; +import org.eclipse.leshan.core.response.ObserveResponse; +import org.eclipse.leshan.core.response.WriteAttributesResponse; +import org.eclipse.leshan.integration.tests.util.LeshanTestClient; +import org.eclipse.leshan.integration.tests.util.LeshanTestServer; +import org.eclipse.leshan.integration.tests.util.LeshanTestServerBuilder; +import org.eclipse.leshan.integration.tests.util.junit5.extensions.BeforeEachParameterizedResolver; +import org.eclipse.leshan.server.registration.Registration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(BeforeEachParameterizedResolver.class) +public class WriteAttributeHouseKeepingTest { + + /*---------------------------------/ + * Parameterized Tests + * -------------------------------*/ + @ParameterizedTest(name = "{0} - Client using {1} - Server using {2}") + @MethodSource("transports") + @Retention(RetentionPolicy.RUNTIME) + private @interface TestAllTransportLayer { + } + + static Stream transports() { + return Stream.of(// + // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "java-coap", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap"), // + arguments(Protocol.COAP, "java-coap", "java-coap")); + } + + /*---------------------------------/ + * Set-up and Tear-down Tests + * -------------------------------*/ + + LeshanTestServer server; + LeshanTestClient client; + Registration currentRegistration; + + @BeforeEach + public void start(Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) { + server = givenServerUsing(givenProtocol).with(givenServerEndpointProvider).build(); + server.start(); + client = givenClientUsing(givenProtocol).with(givenClientEndpointProvider).connectingTo(server).build(); + client.start(); + server.waitForNewRegistrationOf(client); + client.waitForRegistrationTo(server); + + currentRegistration = server.getRegistrationFor(client); + } + + @AfterEach + public void stop() throws InterruptedException { + if (client != null) + client.destroy(false); + if (server != null) + server.destroy(); + } + + protected LeshanTestServerBuilder givenServerUsing(Protocol givenProtocol) { + return new LeshanTestServerBuilder(givenProtocol); + } + + /*---------------------------------/ + * Tests + * -------------------------------*/ + + @TestAllTransportLayer + public void write_attribute_on_tree_then_remove_object_instance(Protocol givenProtocol, + String givenClientEndpointProvider, String givenServerEndpointProvider) throws InterruptedException { + + // For each of this path, + LwM2mPath instancePath = new LwM2mPath(TEST_OBJECT, 0); + LwM2mPath resourcePath = new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE); + LwM2mPath resourceInstancePath = new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE, 0); + List path = Arrays.asList(instancePath, resourcePath, resourceInstancePath); + + // this attribute set will be written, + LwM2mAttributeSet attributeSet = new LwM2mAttributeSet( + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 10l)); + + // Then this request will be executed (to delete node) + DeleteRequest deleteRequest = new DeleteRequest(instancePath); + + // Then ensure there is no more attribute for those path. + write_attribute_on_tree_then_remove_node_then_check_attributes_are_removed(path, attributeSet, deleteRequest); + + } + + @TestAllTransportLayer + public void write_attribute_on_tree_then_remove_resource_instance(Protocol givenProtocol, + String givenClientEndpointProvider, String givenServerEndpointProvider) throws InterruptedException { + + // For each of this path, + LwM2mPath resourceInstancePath = new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE, 0); + List path = Arrays.asList(resourceInstancePath); + + // this attribute set will be written, + LwM2mAttributeSet attributeSet = new LwM2mAttributeSet( + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 10l)); + + // Then this request will be executed (to delete node : we will replace resource instance with id 0 by resource + // instance with id 1) + LwM2mPath resourcePath = new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE); + WriteRequest writeRequest = new WriteRequest(Mode.REPLACE, ContentFormat.TLV, resourcePath, // + new LwM2mMultipleResource(MULTIPLE_INTEGER_VALUE, Type.INTEGER, + LwM2mResourceInstance.newIntegerInstance(1, 10))); + + // Then ensure there is no more attribute for those path. + write_attribute_on_tree_then_remove_node_then_check_attributes_are_removed(path, attributeSet, writeRequest); + } + + private void write_attribute_on_tree_then_remove_node_then_check_attributes_are_removed(List pathToCheck, + LwM2mAttributeSet attributeSet, DownlinkRequest deleteRequest) throws InterruptedException { + + // For each of this path, be sure there is no attributes + LwM2mServer lwServer = client.getServerIdForRegistrationId(currentRegistration.getId()); + for (LwM2mPath path : pathToCheck) { + assertThat(client).hasNoAttributeSetFor(lwServer, path); + } + + // For each of this path, Write Attribute and check it is present. + for (LwM2mPath path : pathToCheck) { + WriteAttributesResponse response = server.send(currentRegistration, + new WriteAttributesRequest(path, attributeSet)); + + assertThat(response).isSuccess(); + assertThat(client).hasAttributesFor(lwServer, path, attributeSet); + } + + // Delete given node + LwM2mResponse response = server.send(currentRegistration, deleteRequest); + assertThat(response).isSuccess(); + + // assert there is no attributes for those path after node deletion + for (LwM2mPath path : pathToCheck) { + assertThat(client).hasNoAttributeSetFor(lwServer, path); + } + } + + @TestAllTransportLayer + public void write_attribute_then_observe_object_then_passive_cancel_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) + throws InterruptedException { + write_attribute_then_observe_then_passive_cancel_then_check_no_more_notification_data( + new LwM2mPath(TEST_OBJECT)); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_object_instance_then_passive_cancel_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) + throws InterruptedException { + + write_attribute_then_observe_then_passive_cancel_then_check_no_more_notification_data( + new LwM2mPath(TEST_OBJECT, 0)); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_resource_then_passive_cancel_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) + throws InterruptedException { + + write_attribute_then_observe_then_passive_cancel_then_check_no_more_notification_data( + new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE)); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_resource_instance_then_passive_cancel_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) + throws InterruptedException { + + write_attribute_then_observe_then_passive_cancel_then_check_no_more_notification_data( + new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE, 0)); + } + + public void write_attribute_then_observe_then_passive_cancel_then_check_no_more_notification_data( + LwM2mPath targetedPath) throws InterruptedException { + + // Add Attribute pmin=1seconds to targeted node + WriteAttributesResponse writeAttributeResponse = server.send(currentRegistration, new WriteAttributesRequest( + targetedPath, new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 1l)))); + assertThat(writeAttributeResponse).isSuccess(); + + // Check there is no notification data + assertThat(client).hasNoNotificationData(); + + // Observe targeted node + ObserveResponse response = server.send(currentRegistration, new ObserveRequest(targetedPath)); + SingleObservation observation = (SingleObservation) server.waitForNewObservation(client); + assertThat(response).isSuccess(); + + // Check that we have new NotificationData + Thread.sleep(100); // wait to be sure client handle observation : TODO We should do better. + assertThat(client).hasNotificationData(); + + // Remove observation at server side : Passive Cancel observation + server.getObservationService().cancelObservation(observation); + + // wait for notification, then assert the is no more notification data + Thread.sleep(1200); // wait to be sure client handle RST (passive cancel) + assertThat(client).hasNoNotificationData(); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_object_then_active_cancel_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) + throws InterruptedException { + + write_attribute_then_observe_then_active_cancel_then_check_no_more_notification_data( + new LwM2mPath(TEST_OBJECT)); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_object_instance_then_active_cancel_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) + throws InterruptedException { + + write_attribute_then_observe_then_active_cancel_then_check_no_more_notification_data( + new LwM2mPath(TEST_OBJECT, 0)); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_resource_then_active_cancel_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) + throws InterruptedException { + + write_attribute_then_observe_then_active_cancel_then_check_no_more_notification_data( + new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE)); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_resource_instance_then_active_cancel_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) + throws InterruptedException { + + write_attribute_then_observe_then_active_cancel_then_check_no_more_notification_data( + new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE, 0)); + } + + public void write_attribute_then_observe_then_active_cancel_then_check_no_more_notification_data( + LwM2mPath targetedPath) throws InterruptedException { + + // Add Attribute pmin=1seconds to targeted node + WriteAttributesResponse writeAttributeResponse = server.send(currentRegistration, new WriteAttributesRequest( + targetedPath, new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 1l)))); + assertThat(writeAttributeResponse).isSuccess(); + + // Check there is no notification data + assertThat(client).hasNoNotificationData(); + + // Observe targeted node + ObserveResponse response = server.send(currentRegistration, new ObserveRequest(targetedPath)); + SingleObservation observation = (SingleObservation) server.waitForNewObservation(client); + assertThat(response).isSuccess(); + + // Check that we have new NotificationData + Thread.sleep(100); // wait to be sure client handle observation : TODO We should do better. + assertThat(client).hasNotificationData(); + + // Do active cancel + CancelObservationResponse cancelObservationResponse = server.send(currentRegistration, + new CancelObservationRequest(observation)); + assertThat(cancelObservationResponse).isSuccess(); + + // then assert the is no more notification data + assertThat(client).hasNoNotificationData(); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_object_then_remove_object_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) + throws InterruptedException { + + write_attribute_then_observe_then_remove_object_then_check_no_more_notification_data( + new LwM2mPath(TEST_OBJECT)); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_object_instance_then_remove_object_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) + throws InterruptedException { + + write_attribute_then_observe_then_remove_object_then_check_no_more_notification_data( + new LwM2mPath(TEST_OBJECT, 0)); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_resource_then_remove_object_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) + throws InterruptedException { + + write_attribute_then_observe_then_remove_object_then_check_no_more_notification_data( + new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE)); + } + + @TestAllTransportLayer + public void write_attribute_then_observe_resource_instance_then_remove_object_then_check_no_more_notification_data( + Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) + throws InterruptedException { + + write_attribute_then_observe_then_remove_object_then_check_no_more_notification_data( + new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE, 0)); + } + + public void write_attribute_then_observe_then_remove_object_then_check_no_more_notification_data( + LwM2mPath targetedPath) throws InterruptedException { + + // Add Attribute pmin=1seconds to targeted node + WriteAttributesResponse writeAttributeResponse = server.send(currentRegistration, new WriteAttributesRequest( + targetedPath, new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 2l)))); + assertThat(writeAttributeResponse).isSuccess(); + + // Check there is no notification data + assertThat(client).hasNoNotificationData(); + + // Observe targeted node + ObserveResponse response = server.send(currentRegistration, new ObserveRequest(targetedPath)); + assertThat(response).isSuccess(); + + // Check that we have new NotificationData + Thread.sleep(100); // wait to be sure client handle observation : TODO We should do better. + assertThat(client).hasNotificationData(); + + // Disable object + client.getObjectTree().removeObjectEnabler(targetedPath.getObjectId()); + + // then assert the is no more notification data + assertThat(client).hasNoNotificationData(); + } + +} diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeObserveTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeObserveTest.java new file mode 100644 index 0000000000..e801ba1691 --- /dev/null +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/attributes/WriteAttributeObserveTest.java @@ -0,0 +1,711 @@ +/******************************************************************************* + * Copyright (c) 2024 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.integration.tests.attributes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.leshan.core.ResponseCode.INTERNAL_SERVER_ERROR; +import static org.eclipse.leshan.core.util.TestLwM2mId.FLOAT_VALUE; +import static org.eclipse.leshan.core.util.TestLwM2mId.INTEGER_VALUE; +import static org.eclipse.leshan.core.util.TestLwM2mId.MULTIPLE_INTEGER_VALUE; +import static org.eclipse.leshan.core.util.TestLwM2mId.TEST_OBJECT; +import static org.eclipse.leshan.core.util.TestLwM2mId.UNSIGNED_INTEGER_VALUE; +import static org.eclipse.leshan.integration.tests.util.LeshanTestClientBuilder.givenClientUsing; +import static org.eclipse.leshan.integration.tests.util.assertion.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.eclipse.leshan.core.endpoint.Protocol; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; +import org.eclipse.leshan.core.node.InvalidLwM2mPathException; +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.request.WriteAttributesRequest; +import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.request.WriteRequest.Mode; +import org.eclipse.leshan.core.request.exception.InvalidRequestException; +import org.eclipse.leshan.core.response.LwM2mResponse; +import org.eclipse.leshan.core.response.ObserveResponse; +import org.eclipse.leshan.core.response.WriteAttributesResponse; +import org.eclipse.leshan.core.util.datatype.ULong; +import org.eclipse.leshan.integration.tests.util.LeshanTestClient; +import org.eclipse.leshan.integration.tests.util.LeshanTestServer; +import org.eclipse.leshan.integration.tests.util.LeshanTestServerBuilder; +import org.eclipse.leshan.integration.tests.util.junit5.extensions.BeforeEachParameterizedResolver; +import org.eclipse.leshan.server.registration.Registration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(BeforeEachParameterizedResolver.class) +public class WriteAttributeObserveTest { + + /*---------------------------------/ + * Parameterized Tests + * -------------------------------*/ + @ParameterizedTest(name = "{0} - Client using {1} - Server using {2}") + @MethodSource("transports") + @Retention(RetentionPolicy.RUNTIME) + private @interface TestAllTransportLayer { + } + + static Stream transports() { + return Stream.of(// + // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider + arguments(Protocol.COAP, "Californium", "Californium"), // + arguments(Protocol.COAP, "java-coap", "Californium"), // + arguments(Protocol.COAP, "Californium", "java-coap"), // + arguments(Protocol.COAP, "java-coap", "java-coap")); + } + + /*---------------------------------/ + * Set-up and Tear-down Tests + * -------------------------------*/ + + LeshanTestServer server; + LeshanTestClient client; + Registration currentRegistration; + + @BeforeEach + public void start(Protocol givenProtocol, String givenClientEndpointProvider, String givenServerEndpointProvider) { + server = givenServerUsing(givenProtocol).with(givenServerEndpointProvider).build(); + server.start(); + client = givenClientUsing(givenProtocol).with(givenClientEndpointProvider).connectingTo(server).build(); + client.start(); + server.waitForNewRegistrationOf(client); + client.waitForRegistrationTo(server); + + currentRegistration = server.getRegistrationFor(client); + + } + + @AfterEach + public void stop() throws InterruptedException { + if (client != null) + client.destroy(false); + if (server != null) + server.destroy(); + } + + protected LeshanTestServerBuilder givenServerUsing(Protocol givenProtocol) { + return new LeshanTestServerBuilder(givenProtocol); + } + + /*---------------------------------/ + * Tests + * -------------------------------*/ + @TestAllTransportLayer + public void test_pmin(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + long pmin = 1l; // seconds + + // Set attribute + WriteAttributesResponse writeAttributeResponse = server.send(currentRegistration, + new WriteAttributesRequest(TEST_OBJECT, 0, INTEGER_VALUE, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, pmin) // + ))); + assertThat(writeAttributeResponse).isSuccess(); + + // Set observe relation + ObserveResponse observeResponse = server.send(currentRegistration, + new ObserveRequest(TEST_OBJECT, 0, INTEGER_VALUE)); + assertThat(observeResponse).isSuccess(); + SingleObservation observation = observeResponse.getObservation(); + assertThat(observation.getPath()).isEqualTo(new LwM2mPath(TEST_OBJECT, 0, INTEGER_VALUE)); + server.waitForNewObservation(observation); + + // Trigger new notification (changing value using Write Request) + LwM2mResponse writeResponse = server.send(currentRegistration, + new WriteRequest(TEST_OBJECT, 0, INTEGER_VALUE, 50l)); + assertThat(writeResponse).isSuccess(); + + // Verify Behavior + ObserveResponse response = server.waitForNotificationExactlyIn(observation, (int) pmin, TimeUnit.SECONDS, 0.2); + assertThat(response.getContent()).isEqualTo(LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 50l)); + assertThat(response).hasValidUnderlyingResponseFor(givenServerEndpointProvider); + } + + @TestAllTransportLayer + public void test_pmax(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + long pmax = 1l; // seconds + + // Set attribute + WriteAttributesResponse writeAttributeResponse = server.send(currentRegistration, + new WriteAttributesRequest(TEST_OBJECT, 0, INTEGER_VALUE, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, pmax) // + ))); + assertThat(writeAttributeResponse).isSuccess(); + + // Set observe relation + ObserveResponse observeResponse = server.send(currentRegistration, + new ObserveRequest(TEST_OBJECT, 0, INTEGER_VALUE)); + assertThat(observeResponse).isSuccess(); + SingleObservation observation = observeResponse.getObservation(); + assertThat(observation.getPath()).isEqualTo(new LwM2mPath(TEST_OBJECT, 0, INTEGER_VALUE)); + server.waitForNewObservation(observation); + + // Do nothing to trigger notification + + // Verify Behavior + ObserveResponse response = server.waitForNotificationExactlyIn(observation, (int) pmax, TimeUnit.SECONDS, 0.2); + assertThat(response.getContent()).isEqualTo(LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 1024l)); + assertThat(response).hasValidUnderlyingResponseFor(givenServerEndpointProvider); + } + + @TestAllTransportLayer + public void test_pmin_and_pmax(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + Long pmin = 1l; // seconds + Long pmax = 2l; // seconds + test_pmin_and_pmax(givenServerEndpointProvider, pmin, pmax); + } + + @TestAllTransportLayer + public void test_pmin_equals_pmax(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + Long pmin = 1l; // seconds + Long pmax = 1l; // seconds + test_pmin_and_pmax(givenServerEndpointProvider, pmin, pmax); + } + + private void test_pmin_and_pmax(String givenServerEndpointProvider, long pmin, long pmax) + throws InvalidRequestException, InterruptedException { + // Setting attribute + WriteAttributesResponse writeAttributeResponse = server.send(currentRegistration, + new WriteAttributesRequest(TEST_OBJECT, 0, INTEGER_VALUE, + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, pmin), + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, pmax)))); + assertThat(writeAttributeResponse).isSuccess(); + + // Setting observe relation + ObserveResponse observeResponse = server.send(currentRegistration, + new ObserveRequest(TEST_OBJECT, 0, INTEGER_VALUE)); + assertThat(observeResponse).isSuccess(); + SingleObservation observation = observeResponse.getObservation(); + assertThat(observation.getPath()).isEqualTo(new LwM2mPath(TEST_OBJECT, 0, INTEGER_VALUE)); + server.waitForNewObservation(observation); + + // Trigger new notification + LwM2mResponse writeResponse = server.send(currentRegistration, + new WriteRequest(TEST_OBJECT, 0, INTEGER_VALUE, 30)); + assertThat(writeResponse).isSuccess(); + + // Verify + ObserveResponse response = server.waitForNotificationExactlyIn(observation, (int) pmin, TimeUnit.SECONDS, 0.2); + assertThat(response.getContent()).isEqualTo(LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 30)); + assertThat(response).hasValidUnderlyingResponseFor(givenServerEndpointProvider); + + // Wait pmax seconds more and check again + response = server.waitForNotificationExactlyIn(observation, (int) pmax, TimeUnit.SECONDS, 0.2); + assertThat(response.getContent()).isEqualTo(LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 30)); + assertThat(response).hasValidUnderlyingResponseFor(givenServerEndpointProvider); + } + + @TestAllTransportLayer + public void test_lt_integer_resource(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + test_first_change_didnt_trigger_then_second_did(givenServerEndpointProvider, // + // target LWM2M node : initial value is 1024 + new LwM2mPath(TEST_OBJECT, 0, INTEGER_VALUE), + // "LESSER THAN" attribute value + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 500d)), + // value which doesn't cross the less_than limit + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 800l), + // value which crosses the less_than limit + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 450l)); + } + + @TestAllTransportLayer + public void test_lt_float_resource(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + test_first_change_didnt_trigger_then_second_did(givenServerEndpointProvider, // + // target LWM2M node : initial value is 3.14159 + new LwM2mPath(TEST_OBJECT, 0, FLOAT_VALUE), + // "LESSER THAN" attribute value + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 2.5d)), + // value which doesn't cross the less_than limit + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, 4.3d), + // value which crosses the less_than limit + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, 2.1d)); + } + + @TestAllTransportLayer + public void test_lt_unsigned_integer_resource(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + test_first_change_didnt_trigger_then_second_did(givenServerEndpointProvider, // + // target LWM2M node : initial value is 9223372036854775808 + new LwM2mPath(TEST_OBJECT, 0, UNSIGNED_INTEGER_VALUE), + // "LESSER THAN" attribute value + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 500d)), + // value which doesn't cross the less_than limit + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, ULong.valueOf(800l)), + // value which crosses the less_than limit + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, ULong.valueOf(450l))); + } + + @TestAllTransportLayer + public void test_gt_integer_resource(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + test_first_change_didnt_trigger_then_second_did(givenServerEndpointProvider, // + // target LWM2M node : initial value is 1024 + new LwM2mPath(TEST_OBJECT, 0, INTEGER_VALUE), + // "GREATER THAN" attribute value + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 2000d)), + // value which doesn't cross the greater_than limit + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 1500l), + // value which crosses the greater_than limit + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 3000l)); + } + + @TestAllTransportLayer + public void test_gt_float_resource(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + test_first_change_didnt_trigger_then_second_did(givenServerEndpointProvider, // + // target LWM2M node : initial value is 3.14159 + new LwM2mPath(TEST_OBJECT, 0, FLOAT_VALUE), + // "GREATER THAN" attribute value + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 5.5d)), + // value which doesn't cross the greater_than limit + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, 4.3d), + // value which crosses the greater_than limit + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, 5.6d)); + } + + @TestAllTransportLayer + public void test_gt_unsigned_integer_resource(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + test_first_change_didnt_trigger_then_second_did(givenServerEndpointProvider, // + // target LWM2M node : initial value is 9223372036854775808 + new LwM2mPath(TEST_OBJECT, 0, UNSIGNED_INTEGER_VALUE), + // "GREATER THAN" attribute value + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 10000000000000000000d)), + // value which doesn't cross the greater_than limit + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, + ULong.valueOf("9999999999999990000")), + // value which crosses the greater_than limit + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, + ULong.valueOf("10000000000000010000"))); + } + + @TestAllTransportLayer + public void test_st_with_positive_gap_on_integer_resource(Protocol givenProtocol, + String givenClientEndpointProvider, String givenServerEndpointProvider) throws InterruptedException { + + test_first_change_didnt_trigger_then_second_did(givenServerEndpointProvider, // + // target LWM2M node : initial value is 1024 + new LwM2mPath(TEST_OBJECT, 0, INTEGER_VALUE), + // "STEP" attribute value + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.STEP, 200d)), + // First change value which isn't a big enough step + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 1124l), + // Second change value which is big enough + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 1225l)); + } + + @TestAllTransportLayer + public void test_st_with_positive_gap_on_float_resource(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + test_first_change_didnt_trigger_then_second_did(givenServerEndpointProvider, // + // target LWM2M node : initial value is 3.14159 + new LwM2mPath(TEST_OBJECT, 0, FLOAT_VALUE), + // "STEP" attribute value + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.STEP, 200d)), + // First change value which isn't a big enough step + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, 103.14159d), + // Second change value which is big enough + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, 204.14159d)); + } + + @TestAllTransportLayer + public void test_st_with_positive_gap_on_unsigned_integer_resource(Protocol givenProtocol, + String givenClientEndpointProvider, String givenServerEndpointProvider) throws InterruptedException { + + test_first_change_didnt_trigger_then_second_did(givenServerEndpointProvider, // + // target LWM2M node : initial value is 9223372036854775808 + new LwM2mPath(TEST_OBJECT, 0, UNSIGNED_INTEGER_VALUE), + // "STEP" attribute value + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.STEP, 20d)), + // First change value which isn't a big enough step + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, + ULong.valueOf("9223372036854775818")), + // Second change value which is big enough + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, + ULong.valueOf("9223372036854775828"))); + } + + @TestAllTransportLayer + public void test_st_with_negative_gap_on_integer_resource(Protocol givenProtocol, + String givenClientEndpointProvider, String givenServerEndpointProvider) throws InterruptedException { + + test_first_change_didnt_trigger_then_second_did(givenServerEndpointProvider, // + // target LWM2M node : initial value is 1024 + new LwM2mPath(TEST_OBJECT, 0, INTEGER_VALUE), + // "STEP" attribute value + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.STEP, 200d)), + // First change value which isn't a big enough step + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 924l), + // Second change value which is big enough + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 823l)); + } + + @TestAllTransportLayer + public void test_st_with_negative_gap_on_float_resource(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + test_first_change_didnt_trigger_then_second_did(givenServerEndpointProvider, // + // target LWM2M node : initial value is 3.14159 + new LwM2mPath(TEST_OBJECT, 0, FLOAT_VALUE), + // "STEP" attribute value + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.STEP, 200d)), + // First change value which isn't a big enough step + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, -103.14159d), + // Second change value which is big enough + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, -203.14159d)); + } + + @TestAllTransportLayer + public void test_st_with_negative_gap_on_unsigned_integer_resource(Protocol givenProtocol, + String givenClientEndpointProvider, String givenServerEndpointProvider) throws InterruptedException { + + test_first_change_didnt_trigger_then_second_did(givenServerEndpointProvider, // + // target LWM2M node : initial value is 9223372036854775808 + new LwM2mPath(TEST_OBJECT, 0, UNSIGNED_INTEGER_VALUE), + // "STEP" attribute value + new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.STEP, 20d)), + // First change value which isn't a big enough step + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, + ULong.valueOf("9223372036854775800")), + // Second change value which is big enough + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, + ULong.valueOf("9223372036854775758"))); + } + + protected void test_first_change_didnt_trigger_then_second_did(String givenServerEndpointProvider, + LwM2mPath targetedResource, LwM2mAttributeSet attributesToWrite, + LwM2mNode valueWhichDidntTriggerNotification, LwM2mNode valueWhichTriggerNotification) + throws InterruptedException { + + // Set attribute + WriteAttributesResponse writeAttributeResponse = server.send(currentRegistration, + new WriteAttributesRequest(targetedResource, attributesToWrite)); + assertThat(writeAttributeResponse).isSuccess(); + + // Set observe relation + ObserveResponse observeResponse = server.send(currentRegistration, new ObserveRequest(targetedResource)); + assertThat(observeResponse).isSuccess(); + SingleObservation observation = observeResponse.getObservation(); + assertThat(observation.getPath()).isEqualTo(targetedResource); + server.waitForNewObservation(observation); + + // Change value which should NOT trigger notification + LwM2mResponse writeResponse = server.send(currentRegistration, new WriteRequest(Mode.REPLACE, ContentFormat.TLV, + targetedResource, valueWhichDidntTriggerNotification)); + assertThat(writeResponse).isSuccess(); + + // Verify Behavior + server.ensureNoNotification(observation, 200, TimeUnit.MILLISECONDS); + + // Change value which should trigger notification + writeResponse = server.send(currentRegistration, + new WriteRequest(Mode.REPLACE, ContentFormat.TLV, targetedResource, valueWhichTriggerNotification)); + assertThat(writeResponse).isSuccess(); + + // Verify Behavior + ObserveResponse response = server.waitForNotificationOf(observation, 100, TimeUnit.MILLISECONDS); + assertThat(response.getContent()).isEqualTo(valueWhichTriggerNotification); + assertThat(response).hasValidUnderlyingResponseFor(givenServerEndpointProvider); + server.ensureNoNotification(observation, 100, TimeUnit.MILLISECONDS); + } + + @TestAllTransportLayer + public void test_lt_pmax_attributes(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + Double lt = 500d; + long pmax = 1l; + + // Setting attribute + WriteAttributesResponse writeAttributeResponse = server.send(currentRegistration, + new WriteAttributesRequest(TEST_OBJECT, 0, INTEGER_VALUE, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, lt), // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, pmax)))); + assertThat(writeAttributeResponse).isSuccess(); + + // Setting observe relation + ObserveResponse observeResponse = server.send(currentRegistration, + new ObserveRequest(TEST_OBJECT, 0, INTEGER_VALUE)); + assertThat(observeResponse).isSuccess(); + SingleObservation observation = observeResponse.getObservation(); + assertThat(observation.getPath()).isEqualTo(new LwM2mPath(TEST_OBJECT, 0, INTEGER_VALUE)); + server.waitForNewObservation(observation); + + // Change value which should NOT trigger notification (initial value is 1024) + LwM2mResponse writeResponse = server.send(currentRegistration, + new WriteRequest(TEST_OBJECT, 0, INTEGER_VALUE, 1200)); + assertThat(writeResponse).isSuccess(); + + // Verify Behavior + server.ensureNoNotification(observation, 200, TimeUnit.MILLISECONDS); + + // Changing value which crosses the lt limit + writeResponse = server.send(currentRegistration, new WriteRequest(TEST_OBJECT, 0, INTEGER_VALUE, 400)); + assertThat(writeResponse).isSuccess(); + + // Verify Behavior + ObserveResponse response = server.waitForNotificationOf(observation, 100, TimeUnit.MILLISECONDS); + assertThat(response.getContent()).isEqualTo(LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 400)); + + // Wait for pmax and ensure new notification is raised + response = server.waitForNotificationExactlyIn(observation, (int) pmax, TimeUnit.SECONDS, 0.2); + + } + + @TestAllTransportLayer + public void test_lt_gt_st_attributes_on_integer_resource(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InvalidLwM2mPathException, InterruptedException { + test_lt_gt_st_attributes(givenServerEndpointProvider, + // target LWM2M node + new LwM2mPath(TEST_OBJECT, 0, INTEGER_VALUE), + // Initial resource value + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 1024l), + // LT + 1000d, + // GT + 1201d, + // ST + 100d, + // value which should not trigger anything + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 1050l), + // value which should LT only + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 990l), + // value which should ST only + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 1100l), + // value which should GT only + LwM2mSingleResource.newIntegerResource(INTEGER_VALUE, 1210l)); + } + + @TestAllTransportLayer + public void test_lt_gt_st_attributes_on_float_resource(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InvalidLwM2mPathException, InterruptedException { + test_lt_gt_st_attributes(givenServerEndpointProvider, + // target LWM2M node + new LwM2mPath(TEST_OBJECT, 0, FLOAT_VALUE), + // Initial resource value + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, 1024d), + // LT + 1000d, + // GT + 1201d, + // ST + 100d, + // value which should not trigger anything + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, 1050d), + // value which should LT only + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, 990d), + // value which should ST only + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, 1100d), + // value which should GT only + LwM2mSingleResource.newFloatResource(FLOAT_VALUE, 1210d)); + } + + @TestAllTransportLayer + public void test_lt_gt_st_attributes_on_unsigned_integer_resource(Protocol givenProtocol, + String givenClientEndpointProvider, String givenServerEndpointProvider) + throws NumberFormatException, InvalidLwM2mPathException, InterruptedException { + test_lt_gt_st_attributes(givenServerEndpointProvider, + // target LWM2M node + new LwM2mPath(TEST_OBJECT, 0, UNSIGNED_INTEGER_VALUE), + // Initial resource value + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, ULong.valueOf("1024")), + // LT + 1000d, + // GT + 1201d, + // ST + 100d, + // value which should not trigger anything + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, ULong.valueOf("1050")), + // value which should LT only + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, ULong.valueOf("990")), + // value which should ST only + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, ULong.valueOf("1100")), + // value which should GT only + LwM2mSingleResource.newUnsignedIntegerResource(UNSIGNED_INTEGER_VALUE, ULong.valueOf("1210"))); + } + + private void test_lt_gt_st_attributes(String givenServerEndpointProvider, LwM2mPath targetedResourcePath, + LwM2mResource initialValue, Double lt, Double gt, Double st, LwM2mResource noTriggerValue, + LwM2mResource triggerLtValue, LwM2mResource triggerStValue, LwM2mResource triggerGtValue) + throws InterruptedException { + + // InitValue + LwM2mResponse writeResponse = server.send(currentRegistration, + new WriteRequest(Mode.REPLACE, ContentFormat.TLV, targetedResourcePath, initialValue)); + assertThat(writeResponse).isSuccess(); + + // Setting attribute + WriteAttributesResponse writeAttributeResponse = server.send(currentRegistration, + new WriteAttributesRequest(targetedResourcePath, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, lt), // + LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, gt), // + LwM2mAttributes.create(LwM2mAttributes.STEP, st)))); + assertThat(writeAttributeResponse).isSuccess(); + + // Setting observe relation + ObserveResponse observeResponse = server.send(currentRegistration, new ObserveRequest(targetedResourcePath)); + assertThat(observeResponse).isSuccess(); + SingleObservation observation = observeResponse.getObservation(); + assertThat(observation.getPath()).isEqualTo(targetedResourcePath); + server.waitForNewObservation(observation); + + // Change value which should NOT trigger notification (initial value is 1024) + writeResponse = server.send(currentRegistration, + new WriteRequest(Mode.REPLACE, ContentFormat.TLV, targetedResourcePath, noTriggerValue)); + assertThat(writeResponse).isSuccess(); + + // Verify Behavior + server.ensureNoNotification(observation, 300, TimeUnit.MILLISECONDS); + + // Changing value which crosses the lt limit but NOT fulfill step condition + writeResponse = server.send(currentRegistration, + new WriteRequest(Mode.REPLACE, ContentFormat.TLV, targetedResourcePath, triggerLtValue)); + assertThat(writeResponse).isSuccess(); + ObserveResponse response = server.waitForNotificationOf(observation, 100, TimeUnit.MILLISECONDS); + assertThat(response).isSuccess(); + + // Changing value which does NOT cross neither gt nor lt but fulfill step condition + writeResponse = server.send(currentRegistration, + new WriteRequest(Mode.REPLACE, ContentFormat.TLV, targetedResourcePath, triggerStValue)); + assertThat(writeResponse).isSuccess(); + response = server.waitForNotificationOf(observation, 100, TimeUnit.MILLISECONDS); + assertThat(response).isSuccess(); + + // Changing value which crosses the gt limit but NOT fulfill step condition + writeResponse = server.send(currentRegistration, + new WriteRequest(Mode.REPLACE, ContentFormat.TLV, targetedResourcePath, triggerGtValue)); + assertThat(writeResponse).isSuccess(); + response = server.waitForNotificationOf(observation, 100, TimeUnit.MILLISECONDS); + assertThat(response).isSuccess(); + + } + + @TestAllTransportLayer + public void test_object_inheritance(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + write_pmax_then_observe_then_wait_for_notification(givenServerEndpointProvider, + // Write pmax to this path + new LwM2mPath(TEST_OBJECT), + // Then observe this path and ensure that pmax behavior is inherited + new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE, 0)); + } + + @TestAllTransportLayer + public void test_object_instance__inheritance(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + write_pmax_then_observe_then_wait_for_notification(givenServerEndpointProvider, + // Write pmax to this path + new LwM2mPath(TEST_OBJECT, 0), + // Then observe this path and ensure that pmax behavior is inherited + new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE, 0)); + } + + @TestAllTransportLayer + public void test_resource__inheritance(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + write_pmax_then_observe_then_wait_for_notification(givenServerEndpointProvider, + // Write pmax to this path + new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE), + // Then observe this path and ensure that pmax behavior is inherited + new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE, 0)); + } + + private void write_pmax_then_observe_then_wait_for_notification(String givenServerEndpointProvider, + LwM2mPath pathToWriteAttribute, LwM2mPath pathToObserve) throws InterruptedException { + + long pmax = 1l; // seconds + + // Set attribute + WriteAttributesResponse writeAttributeResponse = server.send(currentRegistration, + new WriteAttributesRequest(pathToWriteAttribute, new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, pmax) // + ))); + assertThat(writeAttributeResponse).isSuccess(); + + // Set observe relation + ObserveResponse observeResponse = server.send(currentRegistration, new ObserveRequest(pathToObserve)); + assertThat(observeResponse).isSuccess(); + SingleObservation observation = observeResponse.getObservation(); + assertThat(observation.getPath()).isEqualTo(pathToObserve); + server.waitForNewObservation(observation); + + // Do nothing to trigger notification + + // Verify Behavior + ObserveResponse response = server.waitForNotificationExactlyIn(observation, (int) pmax, TimeUnit.SECONDS, 0.2); + assertThat(response).isSuccess(); + } + + @TestAllTransportLayer + public void test_invalid_inheritance_raise_exception(Protocol givenProtocol, String givenClientEndpointProvider, + String givenServerEndpointProvider) throws InterruptedException { + + // Set attribute pmin at resource level > pmax at resource instance level + WriteAttributesResponse writeAttributeResponse = server.send(currentRegistration, + new WriteAttributesRequest(new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE), new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 200l) // + ))); + assertThat(writeAttributeResponse).isSuccess(); + + writeAttributeResponse = server.send(currentRegistration, + new WriteAttributesRequest(new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE, 0), + new LwM2mAttributeSet( // + LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 100l) // + ))); + assertThat(writeAttributeResponse).isSuccess(); + + // Set observe relation + ObserveResponse observeResponse = server.send(currentRegistration, + new ObserveRequest(new LwM2mPath(TEST_OBJECT, 0, MULTIPLE_INTEGER_VALUE, 0))); + assertThat(observeResponse).hasCode(INTERNAL_SERVER_ERROR); + + assertThat(client).hasNoNotificationData(); + } +} diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestClient.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestClient.java index 7235649dd2..68caead446 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestClient.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestClient.java @@ -41,6 +41,7 @@ import org.eclipse.leshan.client.endpoint.LwM2mClientEndpoint; import org.eclipse.leshan.client.endpoint.LwM2mClientEndpointsProvider; import org.eclipse.leshan.client.engine.RegistrationEngineFactory; +import org.eclipse.leshan.client.notification.NotificationDataStore; import org.eclipse.leshan.client.observer.LwM2mClientObserver; import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; import org.eclipse.leshan.client.send.DataSender; @@ -63,6 +64,7 @@ public class LeshanTestClient extends LeshanClient { private final String endpointName; private final InOrder inOrder; private final ReverseProxy proxy; + private NotificationDataStore notificationDataStore; public LeshanTestClient(String endpoint, List objectEnablers, List dataSenders, List trustStore, RegistrationEngineFactory engineFactory, @@ -85,6 +87,16 @@ public LeshanTestClient(String endpoint, List obje inOrder = inOrder(clientObserver); } + @Override + protected NotificationDataStore createNotificationStore() { + notificationDataStore = super.createNotificationStore(); + return notificationDataStore; + } + + public NotificationDataStore getNotificationDataStore() { + return notificationDataStore; + } + public String getEndpointName() { return endpointName; } @@ -191,6 +203,11 @@ public void waitForUpdateFailureTo(LeshanTestServer server, long timeout, TimeUn // ... } + public void waitForBootstrapStarted() { + // TODO Auto-generated method stub + + } + public void waitForBootstrapSuccess(LeshanBootstrapServer server, long timeout, TimeUnit unit) { inOrder.verify(clientObserver, timeout(unit.toMillis(timeout)).times(1)).onBootstrapStarted(assertArg( // s -> assertThat(server.getEndpoints()) // @@ -242,4 +259,5 @@ private boolean isServerIdentifiedByUri(LeshanTestServer server, String expected } return false; } + } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServer.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServer.java index 6697bfb712..ba2ea36ed6 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServer.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServer.java @@ -316,6 +316,15 @@ public ObserveResponse waitForNotificationOf(SingleObservation obs, int timeout, return c.getValue(); } + public ObserveResponse waitForNotificationExactlyIn(SingleObservation observation, int timeout, TimeUnit unit, + double percentOfError) { + SingleObservation singleObservation = observation; + ensureNoNotification(singleObservation, + (int) (TimeUnit.MILLISECONDS.convert(timeout, unit) * (1d - percentOfError)), TimeUnit.MILLISECONDS); + return waitForNotificationOf(singleObservation, + (int) (TimeUnit.MILLISECONDS.convert(timeout, unit) * percentOfError * 2d), TimeUnit.MILLISECONDS); + } + public ObserveResponse waitForNotificationThenCancelled(SingleObservation obs) { return waitForNotificationThenCancelled(obs, 1, TimeUnit.SECONDS); } 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 6aabe75c29..96b2d6717c 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 @@ -36,6 +36,16 @@ protected SELF mySelf() { return (SELF) this; } + public SELF isSuccess() { + isNotNull(); + + if (!actual.isSuccess()) { + failWithMessage("Expected successful Response but was %s %s", actual.getCode(), actual.getErrorMessage()); + } + + return mySelf(); + } + public SELF hasCode(ResponseCode expectedCode) { isNotNull(); diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/assertion/LeshanTestClientAssert.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/assertion/LeshanTestClientAssert.java index 370e050d4c..35846427db 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/assertion/LeshanTestClientAssert.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/assertion/LeshanTestClientAssert.java @@ -18,6 +18,11 @@ import java.util.concurrent.TimeUnit; import org.assertj.core.api.AbstractAssert; +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; +import org.eclipse.leshan.core.link.lwm2m.attributes.NotificationAttributeTree; +import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.integration.tests.util.LeshanTestClient; import org.eclipse.leshan.integration.tests.util.LeshanTestServer; import org.eclipse.leshan.server.LeshanServer; @@ -98,4 +103,59 @@ public LeshanTestClientAssert isSleepingOn(LeshanTestServer server) { return this; } + public void hasNoAttributeSetFor(LwM2mServer server, LwM2mPath path) { + Integer objectId = path.getObjectId(); + if (objectId == null) { + throw new IllegalArgumentException("Path hasn't any object id"); + } + LwM2mObjectEnabler objectEnabler = actual.getObjectTree().getObjectEnabler(objectId); + + if (objectEnabler != null) { + NotificationAttributeTree attributeTree = objectEnabler.getAttributesFor(server); + LwM2mAttributeSet attributeSet = attributeTree.get(path); + if (attributeSet != null && !attributeSet.isEmpty()) { + failWithMessage("Attribute Set for path %s of server %s was expected to be empty but was %s", path, + server.getId(), attributeSet); + } + } + // else if there is no object enabler, there is no more attribute attached. + } + + public void hasAttributesFor(LwM2mServer server, LwM2mPath path, LwM2mAttributeSet expectedAttributeSet) { + Integer objectId = path.getObjectId(); + if (objectId == null) { + throw new IllegalArgumentException("Path hasn't any object id"); + } + LwM2mObjectEnabler objectEnabler = actual.getObjectTree().getObjectEnabler(objectId); + if (objectEnabler == null) { + failWithMessage("%s attribute set was expected for path %s of server %s but there is not object with id %s", + expectedAttributeSet, path, server.getId(), objectId); + } else { + NotificationAttributeTree attributeTree = objectEnabler.getAttributesFor(server); + LwM2mAttributeSet attributeSet = attributeTree.get(path); + if (attributeSet == null || attributeSet.isEmpty()) { + failWithMessage("%s attribute set was expected for path %s of server %s but it is empty", + expectedAttributeSet, path, server.getId()); + } else { + if (!attributeSet.equals(expectedAttributeSet)) { + failWithMessage("%s attribute set was expected for path %s of server %s but it was %s", + expectedAttributeSet, path, server.getId(), attributeSet); + } + } + } + + } + + public void hasNoNotificationData() { + if (!actual.getNotificationDataStore().isEmpty()) { + failWithMessage("Notificatoin Data store should be empty"); + } + } + + public void hasNotificationData() { + if (actual.getNotificationDataStore().isEmpty()) { + failWithMessage("Notificatoin Data store should NOT be empty"); + } + } + } diff --git a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/endpoint/JavaCoapClientEndpointsProvider.java b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/endpoint/JavaCoapClientEndpointsProvider.java index c9176a6618..4ab2ab355d 100644 --- a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/endpoint/JavaCoapClientEndpointsProvider.java +++ b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/endpoint/JavaCoapClientEndpointsProvider.java @@ -24,6 +24,7 @@ import org.eclipse.leshan.client.endpoint.ClientEndpointToolbox; import org.eclipse.leshan.client.endpoint.LwM2mClientEndpoint; import org.eclipse.leshan.client.endpoint.LwM2mClientEndpointsProvider; +import org.eclipse.leshan.client.notification.NotificationManager; import org.eclipse.leshan.client.request.DownlinkRequestReceiver; import org.eclipse.leshan.client.resource.LwM2mObjectTree; import org.eclipse.leshan.client.servers.LwM2mServer; @@ -73,7 +74,7 @@ public class JavaCoapClientEndpointsProvider implements LwM2mClientEndpointsProv @Override public void init(LwM2mObjectTree objectTree, DownlinkRequestReceiver requestReceiver, - ClientEndpointToolbox toolbox) { + NotificationManager notificationManager, ClientEndpointToolbox toolbox) { this.objectTree = objectTree; this.toolbox = toolbox; @@ -122,14 +123,23 @@ public void add(CoapRequest observeRequest) { routerBuilder // .any("/", observersManager.then(new RootResource(requestReceiver, toolbox, identityExtractor))) // .any("/bs", new BootstrapResource(requestReceiver, identityExtractor)) // - .any("/*", observersManager.then(new ObjectResource(requestReceiver, "/", toolbox, identityExtractor))); + .any("/*", observersManager.then(new ObjectResource(requestReceiver, "/", toolbox, identityExtractor, + notificationManager, observersManager))); router = routerBuilder.build(); // Create notification handler NotificationHandler notificationHandler = new NotificationHandler( - // use router but change Observe request in Read request - req -> router.apply(req.withOptions(coapOptionsBuilder -> coapOptionsBuilder.observe(null))), // - observersManager); + // use router but change Observe request in Read request and also flag request as notification + req -> { + TransportContext extendedContext = req.getTransContext() // + .with(LwM2mKeys.LESHAN_NOTIFICATION, true); + + CoapRequest newReq = new CoapRequest(req.getMethod(), req.getToken(), req.options(), + req.getPayload(), req.getPeerAddress(), extendedContext); + + return router.apply(newReq.withOptions(coapOptionsBuilder -> coapOptionsBuilder.observe(null))); + } // + , observersManager); objectTree.addListener(notificationHandler); } diff --git a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/HashMapObserversStore.java b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/HashMapObserversStore.java index 4fbe26acce..01a1bc0b8b 100644 --- a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/HashMapObserversStore.java +++ b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/HashMapObserversStore.java @@ -17,8 +17,10 @@ import java.net.InetSocketAddress; import java.util.Iterator; +import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import com.mbed.coap.packet.CoapRequest; import com.mbed.coap.packet.Opaque; @@ -26,6 +28,7 @@ public class HashMapObserversStore implements ObserversStore { private final ConcurrentHashMap store = new ConcurrentHashMap<>(); + private final List listeners = new CopyOnWriteArrayList<>(); @Override public Iterator iterator() { @@ -34,12 +37,45 @@ public Iterator iterator() { @Override public void add(CoapRequest observeRequest) { - store.put(toKey(observeRequest), observeRequest); + CoapRequest previous = store.put(toKey(observeRequest), observeRequest); + if (previous != null) + fireObserversRemoved(previous); + fireObserversAdded(observeRequest); } @Override public void remove(CoapRequest observeRequest) { - store.remove(toKey(observeRequest)); + CoapRequest previous = store.remove(toKey(observeRequest)); + if (previous != null) { + fireObserversRemoved(previous); + } + } + + @Override + public boolean contains(CoapRequest observeRequest) { + return store.containsKey(toKey(observeRequest)); + } + + @Override + public void addListener(ObserversListener listener) { + listeners.add(listener); + } + + private void fireObserversAdded(CoapRequest observeRequest) { + for (ObserversListener listener : listeners) { + listener.observersAdded(observeRequest); + } + } + + @Override + public void removeListener(ObserversListener listener) { + listeners.remove(listener); + } + + private void fireObserversRemoved(CoapRequest observeRequest) { + for (ObserversListener listener : listeners) { + listener.observersRemoved(observeRequest); + } } protected Object toKey(CoapRequest observeRequest) { @@ -72,5 +108,11 @@ public boolean equals(Object obj) { ObserverKey other = (ObserverKey) obj; return Objects.equals(peerAddress, other.peerAddress) && Objects.equals(token, other.token); } + + @Override + public String toString() { + return String.format("ObserverKey [token=%s, peerAddress=%s]", token != null ? token.toHex() : "null", + peerAddress); + } } } diff --git a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/LwM2mKeys.java b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/LwM2mKeys.java index 00688b3cf8..eb21280273 100644 --- a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/LwM2mKeys.java +++ b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/LwM2mKeys.java @@ -25,4 +25,5 @@ public class LwM2mKeys { // Keys for Observe Request public static final Key> LESHAN_OBSERVED_PATHS = new Key<>(null); + public static final Key LESHAN_NOTIFICATION = new Key<>(null); } diff --git a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/ObserversListener.java b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/ObserversListener.java new file mode 100644 index 0000000000..e233a5c81f --- /dev/null +++ b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/ObserversListener.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2024 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.client.observe; + +import com.mbed.coap.packet.CoapRequest; + +public interface ObserversListener { + + void observersAdded(CoapRequest request); + + void observersRemoved(CoapRequest request); +} diff --git a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/ObserversManager.java b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/ObserversManager.java index b45965e5d2..3646e96451 100644 --- a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/ObserversManager.java +++ b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/ObserversManager.java @@ -63,7 +63,22 @@ public void init(CoapServer server) { @Override public CompletableFuture apply(CoapRequest request, Service service) { - return service.apply(request).thenApply(resp -> subscribe(request, resp)); + CompletableFuture coapResponse = service.apply(request); + if (coapResponse != null) + return coapResponse.thenApply(resp -> subscribe(request, resp)); + return null; + } + + public boolean contains(CoapRequest req) { + return observersStore.contains(req); + } + + public void addListener(ObserversListener listener) { + observersStore.addListener(listener); + } + + public void removeListener(ObserversListener listener) { + observersStore.removeListener(listener); } private CoapResponse subscribe(CoapRequest req, CoapResponse resp) { @@ -90,9 +105,15 @@ public void sendObservation(Predicate observersFilter, } private void sendObservation(CoapRequest observeRequest, Service responseBuilder) { + CompletableFuture coapResponse = responseBuilder.apply(observeRequest); + if (coapResponse != null) { + sendObservation(observeRequest, coapResponse); + } + } + + public void sendObservation(CoapRequest observeRequest, CompletableFuture response) { int currentObserveSequence = observeSeq.incrementAndGet(); - responseBuilder.apply(observeRequest) - .thenApply(obsResponse -> toSeparateResponse(obsResponse, currentObserveSequence, observeRequest)) + response.thenApply(obsResponse -> toSeparateResponse(obsResponse, currentObserveSequence, observeRequest)) .thenAccept(separateResponse -> sendObservation(observeRequest, separateResponse)); } diff --git a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/ObserversStore.java b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/ObserversStore.java index 5cdb082beb..628e23ef76 100644 --- a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/ObserversStore.java +++ b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/observe/ObserversStore.java @@ -22,4 +22,10 @@ public interface ObserversStore extends Iterable { void add(CoapRequest observeRequest); void remove(CoapRequest observeRequest); + + boolean contains(CoapRequest observeRequest); + + void addListener(ObserversListener listener); + + void removeListener(ObserversListener listener); } diff --git a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/resource/ObjectResource.java b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/resource/ObjectResource.java index 1d063efd42..e47c4128a5 100644 --- a/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/resource/ObjectResource.java +++ b/leshan-tl-javacoap-client/src/main/java/org/eclipse/leshan/transport/javacoap/client/resource/ObjectResource.java @@ -20,10 +20,13 @@ import java.util.concurrent.CompletableFuture; import org.eclipse.leshan.client.endpoint.ClientEndpointToolbox; +import org.eclipse.leshan.client.notification.NotificationManager; import org.eclipse.leshan.client.request.DownlinkRequestReceiver; +import org.eclipse.leshan.client.resource.NotificationSender; import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.link.attributes.InvalidAttributeException; +import org.eclipse.leshan.core.link.lwm2m.attributes.InvalidAttributesException; import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; import org.eclipse.leshan.core.node.InvalidLwM2mPathException; import org.eclipse.leshan.core.node.LwM2mNode; @@ -60,7 +63,12 @@ import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.core.response.WriteAttributesResponse; import org.eclipse.leshan.core.response.WriteResponse; +import org.eclipse.leshan.transport.javacoap.client.observe.LwM2mKeys; +import org.eclipse.leshan.transport.javacoap.client.observe.ObserversListener; +import org.eclipse.leshan.transport.javacoap.client.observe.ObserversManager; 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; @@ -69,14 +77,52 @@ public class ObjectResource extends LwM2mClientCoapResource { + private static final Logger LOG = LoggerFactory.getLogger(ObjectResource.class); + protected DownlinkRequestReceiver requestReceiver; protected ClientEndpointToolbox toolbox; + protected NotificationManager notificationManager; + protected ObserversManager observersManager; public ObjectResource(DownlinkRequestReceiver requestReceiver, String uri, ClientEndpointToolbox toolbox, - ServerIdentityExtractor serverIdentityExtractor) { + ServerIdentityExtractor serverIdentityExtractor, NotificationManager notificationManager, + ObserversManager observersManager) { super(uri, serverIdentityExtractor); this.requestReceiver = requestReceiver; this.toolbox = toolbox; + this.notificationManager = notificationManager; + this.observersManager = observersManager; + + this.observersManager.addListener(new ObserversListener() { + @Override + public void observersRemoved(CoapRequest coapRequest) { + // Get object URI + String URI = coapRequest.options().getUriPath(); + // we don't manage observation on root path + if (URI == null) + return; + + // Get Server identity + LwM2mServer extractIdentity = extractIdentity(coapRequest); + + // handle content format for Read and Observe Request + ContentFormat requestedContentFormat = null; + if (coapRequest.options().getAccept() != null) { + // If an request ask for a specific content format, use it (if we support it) + requestedContentFormat = ContentFormat.fromCode(coapRequest.options().getAccept()); + } + + // Create Observe request + ObserveRequest observeRequest = new ObserveRequest(requestedContentFormat, URI, coapRequest); + + // Remove notification data for this request + notificationManager.clear(extractIdentity, observeRequest); + } + + @Override + public void observersAdded(CoapRequest request) { + } + }); } @Override @@ -139,14 +185,36 @@ public CompletableFuture handleGET(CoapRequest coapRequest) { ObserveResponse response = requestReceiver.requestReceived(identity, observeRequest).getResponse(); if (response.getCode() == ResponseCode.CONTENT) { ContentFormat format = getContentFormat(observeRequest, requestedContentFormat); - return responseWithPayload( // + CompletableFuture coapResponse = responseWithPayload( // response.getCode(), // format, // toolbox.getEncoder().encode(response.getContent(), format, getPath(URI), toolbox.getModel())); + + // store observation relation if this is not a active observe cancellation + if (coapRequest.options().getObserve() != 1) { + try { + notificationManager.initRelation(identity, observeRequest, response.getContent(), + createNotificationSender(coapRequest, identity, observeRequest, + requestedContentFormat)); + } catch (InvalidAttributesException e) { + return errorMessage(ResponseCode.INTERNAL_SERVER_ERROR, + "Invalid Attributes state : " + e.getMessage()); + } + } + return coapResponse; } else { - return errorMessage(response.getCode(), response.getErrorMessage()); + CompletableFuture errorMessage = errorMessage(response.getCode(), + response.getErrorMessage()); + notificationManager.clear(identity, observeRequest); + return errorMessage; } + } else if (coapRequest.getTransContext(LwM2mKeys.LESHAN_NOTIFICATION, false)) { + // Manage Notifications + ObserveRequest observeRequest = new ObserveRequest(requestedContentFormat, URI, coapRequest); + notificationManager.notificationTriggered(identity, observeRequest, + createNotificationSender(coapRequest, identity, observeRequest, requestedContentFormat)); + return null; } else { if (identity.isLwm2mBootstrapServer()) { // Manage Bootstrap Read Request @@ -181,6 +249,7 @@ public CompletableFuture handleGET(CoapRequest coapRequest) { } } } + } protected ContentFormat getContentFormat(DownlinkRequest request, ContentFormat requestedContentFormat) { @@ -425,4 +494,41 @@ public CompletableFuture handleDELETE(CoapRequest coapRequest) { } } } + + protected NotificationSender createNotificationSender(CoapRequest coapRequest, LwM2mServer server, + ObserveRequest observeRequest, ContentFormat requestedContentFormat) { + return new NotificationSender() { + @Override + public boolean sendNotification(ObserveResponse response) { + try { + if (observersManager.contains(coapRequest)) + if (response.getCode() == ResponseCode.CONTENT) { + ContentFormat format = getContentFormat(observeRequest, requestedContentFormat); + CompletableFuture coapResponse = responseWithPayload( // + response.getCode(), // + format, // + toolbox.getEncoder().encode(response.getContent(), format, + getPath(coapRequest.options().getUriPath()), toolbox.getModel())); + + // store observation relation + observersManager.sendObservation(coapRequest, coapResponse); + return true; + } else { + CompletableFuture errorMessage = errorMessage(response.getCode(), + response.getErrorMessage()); + observersManager.sendObservation(coapRequest, errorMessage); + return false; + } + else { + return false; + } + } catch (Exception e) { + LOG.error("Exception while sending notification [{}] for [{}] to {}", response, observeRequest, + server, e); + errorMessage(ResponseCode.INTERNAL_SERVER_ERROR, "failure sending notification"); + return false; + } + } + }; + } }