From 2a39eba839b60b7f3822ce0936cfe48d6f2c8cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wadowski?= Date: Wed, 28 Jul 2021 14:47:59 +0200 Subject: [PATCH 1/8] Add Single/Composite Observation, ObserveComposite Request/Response classes. --- .../eclipse/leshan/core/node/LwM2mPath.java | 20 +++ .../observation/CompositeObservation.java | 86 ++++++++++++ .../leshan/core/observation/Observation.java | 52 +++---- .../core/observation/SingleObservation.java | 86 ++++++++++++ .../request/CancelObservationRequest.java | 3 +- .../DownLinkRequestVisitorAdapter.java | 5 + .../core/request/DownlinkRequestVisitor.java | 3 + .../core/request/ObserveCompositeRequest.java | 131 ++++++++++++++++++ .../leshan/core/request/ObserveRequest.java | 5 + .../core/request/ReadCompositeRequest.java | 43 +++--- .../response/CancelObservationResponse.java | 6 +- .../response/ObserveCompositeResponse.java | 98 +++++++++++++ .../leshan/core/response/ObserveResponse.java | 10 +- .../ObserveCompositeResponseTest.java | 44 ++++++ .../core/response/ObserveResponseTest.java | 100 +++++++++++++ .../integration/tests/RegistrationTest.java | 3 +- .../tests/observe/ObserveTest.java | 87 ++++++------ .../tests/observe/ObserveTimeStampTest.java | 37 ++--- .../observe/TestObservationListener.java | 34 ++++- .../redis/RedisRegistrationStoreTest.java | 126 +++++++++++++++++ .../util/RedisIntegrationTestHelper.java | 12 +- .../tests/write/WriteCompositeTest.java | 12 +- .../observation/ObservationServiceImpl.java | 62 ++++++--- .../californium/observation/ObserveUtil.java | 76 ++++++---- .../InMemoryRegistrationStore.java | 11 +- .../request/CoapRequestBuilder.java | 6 + .../request/LwM2mResponseBuilder.java | 10 +- .../server/californium/DummyDecoder.java | 71 ++++++++++ .../observation/ObservationServiceTest.java | 92 +++++++++++- .../observation/ObserveUtilTest.java | 93 +++++++++++++ .../InMemoryRegistrationStoreTest.java | 97 +++++++++++-- .../request/LwM2mResponseBuilderTest.java | 65 +++++++++ .../observation/ObservationListener.java | 15 +- .../server/queue/PresenceStateListener.java | 16 ++- .../server/demo/servlet/EventServlet.java | 56 +++++++- .../server/redis/RedisRegistrationStore.java | 11 +- .../serialization/RegistrationSerDesTest.java | 4 +- .../serialization/SecurityInfoSerDesTest.java | 1 - 38 files changed, 1458 insertions(+), 231 deletions(-) create mode 100644 leshan-core/src/main/java/org/eclipse/leshan/core/observation/CompositeObservation.java create mode 100644 leshan-core/src/main/java/org/eclipse/leshan/core/observation/SingleObservation.java create mode 100644 leshan-core/src/main/java/org/eclipse/leshan/core/request/ObserveCompositeRequest.java create mode 100644 leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveCompositeResponse.java create mode 100644 leshan-core/src/test/java/org/eclipse/leshan/core/response/ObserveCompositeResponseTest.java create mode 100644 leshan-core/src/test/java/org/eclipse/leshan/core/response/ObserveResponseTest.java create mode 100644 leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/server/redis/RedisRegistrationStoreTest.java create mode 100644 leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/DummyDecoder.java create mode 100644 leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java create mode 100644 leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilderTest.java diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mPath.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mPath.java index 18ca4f36f7..1b37315b87 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mPath.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mPath.java @@ -15,6 +15,9 @@ *******************************************************************************/ package org.eclipse.leshan.core.node; +import java.util.ArrayList; +import java.util.List; + import org.eclipse.leshan.core.util.Validate; /** @@ -422,4 +425,21 @@ public static LwM2mPath parse(String fullpath, String lwm2mRootpath) return new LwM2mPath(path); } + + /** + * Create list of LwM2mPath from list of paths + * + * @param paths list of paths as {@link String}. + * @return list of paths as {@link LwM2mPath}. + * + * @exception LwM2mNodeException if path is invalid (e.g. too big number in path) + * @exception IllegalArgumentException if path length is invalid or if path contains not Numeric value + */ + public static List getLwM2mPathList(List paths) { + List res = new ArrayList<>(paths.size()); + for (String path : paths) { + res.add(new LwM2mPath(path)); + } + return res; + } } \ No newline at end of file diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/CompositeObservation.java b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/CompositeObservation.java new file mode 100644 index 0000000000..3b33c700e1 --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/CompositeObservation.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2021 Orange. + * + * 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: + * Michał Wadowski (Orange) - Add Observe-Composite feature. + *******************************************************************************/ +package org.eclipse.leshan.core.observation; + +import java.util.List; +import java.util.Map; + +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.util.Hex; + +/** + * An composite-observation of a resource provided by a LWM2M Client. + */ +public class CompositeObservation extends Observation { + + private final List paths; + + /** + * Instantiates an {@link CompositeObservation} for the given node paths. + * + * @param id token identifier of the observation + * @param registrationId client's unique registration identifier. + * @param paths resources paths for which the composite-observation is set. + * @param contentFormat contentFormat used to read the resource (could be null). + * @param context additional information relative to this observation. + */ + public CompositeObservation(byte[] id, String registrationId, List paths, ContentFormat contentFormat, + Map context) { + super(id, registrationId, contentFormat, context); + this.paths = paths; + } + + /** + * Gets the observed resources paths. + * + * @return the resources paths + */ + public List getPaths() { + return paths; + } + + @Override + public String toString() { + return String.format("CompositeObservation [paths=%s, id=%s, contentFormat=%s, registrationId=%s, context=%s]", + paths, Hex.encodeHexString(id), contentFormat, registrationId, context); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((paths == null) ? 0 : paths.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + CompositeObservation other = (CompositeObservation) obj; + if (paths == null) { + if (other.paths != null) + return false; + } else if (!paths.equals(other.paths)) + return false; + return true; + } +} diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/Observation.java b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/Observation.java index c26dff525f..d588b74ba0 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/Observation.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/Observation.java @@ -12,6 +12,7 @@ * * Contributors: * Sierra Wireless - initial API and implementation + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.core.observation; @@ -20,34 +21,28 @@ import java.util.HashMap; import java.util.Map; -import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.request.ContentFormat; -import org.eclipse.leshan.core.util.Hex; /** - * An observation of a resource provided by a LWM2M Client. + * An abstract class for observation of a resource provided by a LWM2M Client. */ -public class Observation { +public abstract class Observation { - private final byte[] id; - private final LwM2mPath path; - private final ContentFormat contentFormat; - private final String registrationId; - private final Map context; + protected final byte[] id; + protected final ContentFormat contentFormat; + protected final String registrationId; + protected final Map context; /** - * Instantiates an {@link Observation} for the given node path. - * + * An abstract constructor for {@link Observation}. + * * @param id token identifier of the observation * @param registrationId client's unique registration identifier. - * @param path resource path for which the observation is set. * @param contentFormat contentFormat used to read the resource (could be null). * @param context additional information relative to this observation. */ - public Observation(byte[] id, String registrationId, LwM2mPath path, ContentFormat contentFormat, - Map context) { + public Observation(byte[] id, String registrationId, ContentFormat contentFormat, Map context) { this.id = id; - this.path = path; this.contentFormat = contentFormat; this.registrationId = registrationId; if (context != null) @@ -73,15 +68,6 @@ public String getRegistrationId() { return registrationId; } - /** - * Gets the observed resource path. - * - * @return the resource path - */ - public LwM2mPath getPath() { - return path; - } - /** * Gets the requested contentFormat (could be null). * @@ -98,19 +84,13 @@ public Map getContext() { return context; } - @Override - public String toString() { - return String.format("Observation [id=%s, path=%s, registrationId=%s, contentFormat=%s context=%s]", - Hex.encodeHexString(id), path, registrationId, contentFormat, context); - } - @Override public int hashCode() { final int prime = 31; int result = 1; + result = prime * result + ((contentFormat == null) ? 0 : contentFormat.hashCode()); result = prime * result + ((context == null) ? 0 : context.hashCode()); result = prime * result + Arrays.hashCode(id); - result = prime * result + ((path == null) ? 0 : path.hashCode()); result = prime * result + ((registrationId == null) ? 0 : registrationId.hashCode()); return result; } @@ -124,6 +104,11 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) return false; Observation other = (Observation) obj; + if (contentFormat == null) { + if (other.contentFormat != null) + return false; + } else if (!contentFormat.equals(other.contentFormat)) + return false; if (context == null) { if (other.context != null) return false; @@ -131,11 +116,6 @@ public boolean equals(Object obj) { return false; if (!Arrays.equals(id, other.id)) return false; - if (path == null) { - if (other.path != null) - return false; - } else if (!path.equals(other.path)) - return false; if (registrationId == null) { if (other.registrationId != null) return false; diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/SingleObservation.java b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/SingleObservation.java new file mode 100644 index 0000000000..58c957c921 --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/SingleObservation.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2013-2015 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) - Add Observe-Composite feature. + *******************************************************************************/ +package org.eclipse.leshan.core.observation; + +import java.util.Map; + +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.util.Hex; + +/** + * An observation of a resource provided by a LWM2M Client. + */ +public class SingleObservation extends Observation { + + private final LwM2mPath path; + + /** + * Instantiates an {@link SingleObservation} for the given node path. + * + * @param id token identifier of the observation + * @param registrationId client's unique registration identifier. + * @param path resource path for which the observation is set. + * @param contentFormat contentFormat used to read the resource (could be null). + * @param context additional information relative to this observation. + */ + public SingleObservation(byte[] id, String registrationId, LwM2mPath path, ContentFormat contentFormat, + Map context) { + super(id, registrationId, contentFormat, context); + this.path = path; + } + + /** + * Gets the observed resource path. + * + * @return the resource path + */ + public LwM2mPath getPath() { + return path; + } + + @Override + public String toString() { + return String.format("SingleObservation [path=%s, id=%s, contentFormat=%s, registrationId=%s, context=%s]", + path, Hex.encodeHexString(id), contentFormat, registrationId, context); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((path == null) ? 0 : path.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + SingleObservation other = (SingleObservation) obj; + if (path == null) { + if (other.path != null) + return false; + } else if (!path.equals(other.path)) + return false; + return true; + } +} diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelObservationRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelObservationRequest.java index 3f60300ab4..efe7c5a061 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelObservationRequest.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelObservationRequest.java @@ -16,6 +16,7 @@ package org.eclipse.leshan.core.request; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.exception.InvalidRequestException; import org.eclipse.leshan.core.response.CancelObservationResponse; import org.eclipse.leshan.core.util.Hex; @@ -34,7 +35,7 @@ public class CancelObservationRequest extends AbstractSimpleDownlinkRequest + implements CompositeDownlinkRequest { + + private final ContentFormat requestContentFormat; + private final ContentFormat responseContentFormat; + + private final List paths; + + private final Map context; + + /** + * Create ObserveCompositeRequest Request. + * + * @param requestContentFormat The {@link ContentFormat} used to encode the list of {@link LwM2mPath} + * @param responseContentFormat The {@link ContentFormat} requested to encode the {@link LwM2mNode} of the response. + * @param paths List of {@link LwM2mPath} corresponding to {@link LwM2mNode} to read. + * @exception InvalidRequestException if path has invalid format. + * + */ + public ObserveCompositeRequest(ContentFormat requestContentFormat, ContentFormat responseContentFormat, + String... paths) { + this(requestContentFormat, responseContentFormat, getLwM2mPathList(Arrays.asList(paths))); + } + + private static List getLwM2mPathList(List paths) { + try { + return LwM2mPath.getLwM2mPathList(paths); + } catch (LwM2mNodeException | IllegalArgumentException e) { + throw new InvalidRequestException("invalid path format"); + } + } + + /** + * Create ObserveCompositeRequest Request. + * + * @param requestContentFormat The {@link ContentFormat} used to encode the list of {@link LwM2mPath} + * @param responseContentFormat The {@link ContentFormat} requested to encode the {@link LwM2mNode} of the response. + * @param paths List of {@link LwM2mPath} corresponding to {@link LwM2mNode} to read. + * + */ + public ObserveCompositeRequest(ContentFormat requestContentFormat, ContentFormat responseContentFormat, + List paths) { + this(requestContentFormat, responseContentFormat, paths, null); + } + + /** + * Create ObserveCompositeRequest Request. + * + * @param requestContentFormat The {@link ContentFormat} used to encode the list of {@link LwM2mPath} + * @param responseContentFormat The {@link ContentFormat} requested to encode the {@link LwM2mNode} of the response. + * @param paths List of {@link LwM2mPath} corresponding to {@link LwM2mNode} to read. + * @param coapRequest the underlying request. + * + */ + public ObserveCompositeRequest(ContentFormat requestContentFormat, ContentFormat responseContentFormat, + List paths, Object coapRequest) { + super(coapRequest); + + this.requestContentFormat = requestContentFormat; + this.responseContentFormat = responseContentFormat; + this.paths = paths; + + this.context = Collections.emptyMap(); + } + + @Override + public void accept(DownlinkRequestVisitor visitor) { + visitor.visit(this); + } + + /** + * @return List of {@link LwM2mPath} corresponding to {@link LwM2mNode} to read. + */ + @Override + public List getPaths() { + return paths; + } + + /** + * @return the {@link ContentFormat} used to encode the list of {@link LwM2mPath} + */ + public ContentFormat getRequestContentFormat() { + return requestContentFormat; + } + + /** + * @return the {@link ContentFormat} requested to encode the {@link LwM2mNode} of the response. + */ + public ContentFormat getResponseContentFormat() { + return responseContentFormat; + } + + /** + * @return map containing the additional information relative to this observe-composite request. + */ + public Map getContext() { + return context; + } +} diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ObserveRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ObserveRequest.java index 0599e0e6af..8ce7d7601b 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ObserveRequest.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ObserveRequest.java @@ -170,6 +170,11 @@ public ObserveRequest(ContentFormat format, String path, Map con this(format, newPath(path), context, null); } + public ObserveRequest(ContentFormat format, LwM2mPath path, Object coapRequest) + throws InvalidRequestException { + this(format, path, null, coapRequest); + } + private ObserveRequest(ContentFormat format, LwM2mPath target, Map context, Object coapRequest) { super(target, coapRequest); if (target.isRoot()) diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ReadCompositeRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ReadCompositeRequest.java index b9b7d9e5ca..4e7ac61ffc 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ReadCompositeRequest.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ReadCompositeRequest.java @@ -15,10 +15,11 @@ *******************************************************************************/ package org.eclipse.leshan.core.request; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mNodeException; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.request.exception.InvalidRequestException; import org.eclipse.leshan.core.response.ReadCompositeResponse; @@ -40,11 +41,12 @@ public class ReadCompositeRequest extends AbstractLwM2mRequest paths) { - this(newPaths(paths), requestContentFormat, responseContentFormat, null); + this(getLwM2mPathList(paths), requestContentFormat, responseContentFormat, null); + } + + private static List getLwM2mPathList(List paths) { + try { + return LwM2mPath.getLwM2mPathList(paths); + } catch (LwM2mNodeException | IllegalArgumentException e) { + throw new InvalidRequestException("invalid path format"); + } } /** @@ -69,7 +80,8 @@ public ReadCompositeRequest(ContentFormat requestContentFormat, ContentFormat re * @param requestContentFormat The {@link ContentFormat} used to encode the list of {@link LwM2mPath} * @param responseContentFormat The {@link ContentFormat} requested to encode the {@link LwM2mNode} of the response. * @param coapRequest the underlying request. - * + * @exception InvalidRequestException if paths list is invalid. + * */ public ReadCompositeRequest(List paths, ContentFormat requestContentFormat, ContentFormat responseContentFormat, Object coapRequest) { @@ -154,27 +166,4 @@ public boolean equals(Object obj) { return true; } - protected static List newPaths(List paths) { - try { - List res = new ArrayList<>(paths.size()); - for (String path : paths) { - res.add(new LwM2mPath(path)); - } - return res; - } catch (IllegalArgumentException e) { - throw new InvalidRequestException(); - } - } - - protected static List newPaths(String[] paths) { - try { - List res = new ArrayList<>(paths.length); - for (String path : paths) { - res.add(new LwM2mPath(path)); - } - return res; - } catch (IllegalArgumentException e) { - throw new InvalidRequestException(); - } - } } diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/response/CancelObservationResponse.java b/leshan-core/src/main/java/org/eclipse/leshan/core/response/CancelObservationResponse.java index fd60670631..1384ea286f 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/response/CancelObservationResponse.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/response/CancelObservationResponse.java @@ -20,17 +20,17 @@ import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; -import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; public class CancelObservationResponse extends ObserveResponse { public CancelObservationResponse(ResponseCode code, LwM2mNode content, List timestampedValues, - Observation observation, String errorMessage) { + SingleObservation observation, String errorMessage) { super(code, content, timestampedValues, observation, errorMessage); } public CancelObservationResponse(ResponseCode code, LwM2mNode content, List timestampedValues, - Observation observation, String errorMessage, Object coapResponse) { + SingleObservation observation, String errorMessage, Object coapResponse) { super(code, content, timestampedValues, observation, errorMessage, coapResponse); } diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveCompositeResponse.java b/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveCompositeResponse.java new file mode 100644 index 0000000000..586c7d1904 --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveCompositeResponse.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright (c) 2021 Orange. + * + * 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: + * Michał Wadowski (Orange) - Add Observe-Composite feature. + *******************************************************************************/ +package org.eclipse.leshan.core.response; + +import java.util.Map; + +import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; + +/** + * Specialized ReadCompositeResponse to a Observe-Composite request, with the corresponding Observation. + * + * This can be useful to listen to updates on the specific Observation. + */ +public class ObserveCompositeResponse extends ReadCompositeResponse { + + protected final CompositeObservation observation; + + public ObserveCompositeResponse(ResponseCode code, Map content, String errorMessage, + Object coapResponse, CompositeObservation observation) { + super(code, content, errorMessage, coapResponse); + this.observation = observation; + } + + public CompositeObservation getObservation() { + return observation; + } + + @Override + public String toString() { + if (errorMessage != null) + return String.format("ObserveCompositeResponse [code=%s, errorMessage=%s]", code, errorMessage); + else + return String.format("ObserveCompositeResponse [code=%s, observation=%s, content=%s]", code, observation, + content); + } + + @Override + public boolean isValid() { + switch (code.getCode()) { + case ResponseCode.CONTENT_CODE: + case ResponseCode.BAD_REQUEST_CODE: + case ResponseCode.NOT_FOUND_CODE: + case ResponseCode.UNAUTHORIZED_CODE: + case ResponseCode.METHOD_NOT_ALLOWED_CODE: + case ResponseCode.UNSUPPORTED_CONTENT_FORMAT_CODE: + case ResponseCode.INTERNAL_SERVER_ERROR_CODE: + return true; + default: + return false; + } + } + + // Syntactic sugar static constructors: + + public static ObserveCompositeResponse success(Map content) { + return new ObserveCompositeResponse(ResponseCode.CONTENT, content, null, null, null); + } + + public static ObserveCompositeResponse badRequest(String errorMessage) { + return new ObserveCompositeResponse(ResponseCode.BAD_REQUEST, null, errorMessage, null, null); + } + + public static ObserveCompositeResponse notFound() { + return new ObserveCompositeResponse(ResponseCode.NOT_FOUND, null, null, null, null); + } + + public static ObserveCompositeResponse unauthorized() { + return new ObserveCompositeResponse(ResponseCode.UNAUTHORIZED, null, null, null, null); + } + + public static ObserveCompositeResponse methodNotAllowed() { + return new ObserveCompositeResponse(ResponseCode.METHOD_NOT_ALLOWED, null, null, null, null); + } + + public static ObserveCompositeResponse notAcceptable() { + return new ObserveCompositeResponse(ResponseCode.UNSUPPORTED_CONTENT_FORMAT, null, null, null, null); + } + + public static ObserveCompositeResponse internalServerError(String errorMessage) { + return new ObserveCompositeResponse(ResponseCode.INTERNAL_SERVER_ERROR, null, errorMessage, null, null); + } +} diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveResponse.java b/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveResponse.java index 1a865d093f..e03aef11b5 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveResponse.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/response/ObserveResponse.java @@ -20,7 +20,7 @@ import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; -import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.exception.InvalidResponseException; /** @@ -30,16 +30,16 @@ */ public class ObserveResponse extends ReadResponse { - protected final Observation observation; + protected final SingleObservation observation; protected final List timestampedValues; public ObserveResponse(ResponseCode code, LwM2mNode content, List timestampedValues, - Observation observation, String errorMessage) { + SingleObservation observation, String errorMessage) { this(code, content, timestampedValues, observation, errorMessage, null); } public ObserveResponse(ResponseCode code, LwM2mNode content, List timestampedValues, - Observation observation, String errorMessage, Object coapResponse) { + SingleObservation observation, String errorMessage, Object coapResponse) { super(code, timestampedValues != null && !timestampedValues.isEmpty() ? timestampedValues.get(0).getNode() : content, errorMessage, coapResponse); @@ -74,7 +74,7 @@ else if (timestampedValues != null) return String.format("ObserveResponse [code=%s, content=%s, observation=%s]", code, content, observation); } - public Observation getObservation() { + public SingleObservation getObservation() { return observation; } diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/response/ObserveCompositeResponseTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/response/ObserveCompositeResponseTest.java new file mode 100644 index 0000000000..4cd14a34ef --- /dev/null +++ b/leshan-core/src/test/java/org/eclipse/leshan/core/response/ObserveCompositeResponseTest.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2021 Orange. + * + * 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: + * Michał Wadowski (Orange) - Add Observe-Composite feature. + *******************************************************************************/ +package org.eclipse.leshan.core.response; + +import static org.eclipse.leshan.core.ResponseCode.CONTENT; +import static org.eclipse.leshan.core.node.LwM2mSingleResource.newResource; +import static org.junit.Assert.assertEquals; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.junit.Test; + +public class ObserveCompositeResponseTest { + + @Test + public void should_create_response_with_content() { + // given + Map exampleContent = new HashMap<>(); + exampleContent.put(new LwM2mPath("/1/2/3"), newResource(15, "example 1")); + exampleContent.put(new LwM2mPath("/2/3/4"), newResource(16, "example 2")); + + // when + ObserveCompositeResponse response = new ObserveCompositeResponse(CONTENT, exampleContent, null, null, null); + + // then + assertEquals(exampleContent, response.getContent()); + } +} diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/response/ObserveResponseTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/response/ObserveResponseTest.java new file mode 100644 index 0000000000..012060f272 --- /dev/null +++ b/leshan-core/src/test/java/org/eclipse/leshan/core/response/ObserveResponseTest.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2021 Orange. + * + * 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: + * Michał Wadowski (Orange) - Add Observe-Composite feature. + *******************************************************************************/ +package org.eclipse.leshan.core.response; + +import static org.eclipse.leshan.core.ResponseCode.*; +import static org.eclipse.leshan.core.node.LwM2mSingleResource.newResource; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.node.TimestampedLwM2mNode; +import org.eclipse.leshan.core.request.exception.InvalidResponseException; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ObserveResponseTest { + + @Parameters(name = "{0}") + public static Collection responseCodes() { + return Arrays.asList(CONTENT, CHANGED); + } + + private final ResponseCode responseCode; + + public ObserveResponseTest(ResponseCode responseCode) { + this.responseCode = responseCode; + } + + @Test + public void should_throw_invalid_response_exception_if_no_content() { + assertThrows(InvalidResponseException.class, new ThrowingRunnable() { + @Override + public void run() { + new ObserveResponse(responseCode, null, null, null, null); + } + }); + } + + @Test + public void should_throw_invalid_response_exception_if_no_content_and_empty_timestamped_values() { + assertThrows(InvalidResponseException.class, new ThrowingRunnable() { + @Override + public void run() { + new ObserveResponse(responseCode, null, Collections. emptyList(), null, null); + } + }); + } + + @Test + public void should_not_throw_exception_if_has_content() { + // given + LwM2mSingleResource exampleResource = newResource(15, "example"); + + // when + ObserveResponse response = new ObserveResponse(responseCode, exampleResource, null, null, null); + + // then + assertEquals(exampleResource, response.getContent()); + assertNull(response.getTimestampedLwM2mNode()); + } + + @Test + public void should_get_content_from_first_of_timestamped_values() { + // given + List timestampedValues = Arrays.asList( + new TimestampedLwM2mNode(123L, newResource(15, "example 1")), + new TimestampedLwM2mNode(456L, newResource(15, "example 2"))); + + LwM2mSingleResource content = responseCode == CHANGED ? newResource(15, "example 1") : null; + + // when + ObserveResponse response = new ObserveResponse(responseCode, content, timestampedValues, null, null); + + // then + assertEquals(timestampedValues.get(0).getNode(), response.getContent()); + assertEquals(timestampedValues, response.getTimestampedLwM2mNode()); + } +} diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RegistrationTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RegistrationTest.java index 4a0dcdc0af..8fd6612974 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RegistrationTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RegistrationTest.java @@ -40,6 +40,7 @@ import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.core.request.ReadRequest; @@ -220,7 +221,7 @@ public void register_observe_deregister_observe() throws NonUniqueSecurityInfoEx Registration currentRegistration = helper.getCurrentRegistration(); Set observations = helper.server.getObservationService().getObservations(currentRegistration); assertEquals(1, observations.size()); - Observation obs = observations.iterator().next(); + SingleObservation obs = (SingleObservation) observations.iterator().next(); assertEquals(currentRegistration.getId(), obs.getRegistrationId()); assertEquals(new LwM2mPath(3, 0), obs.getPath()); diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java index 200e115fb0..3b150024bb 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java @@ -38,6 +38,7 @@ import org.eclipse.leshan.core.node.codec.LwM2mValueChecker; import org.eclipse.leshan.core.node.codec.json.LwM2mNodeJsonEncoder; import org.eclipse.leshan.core.observation.Observation; +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.ObserveRequest; @@ -87,12 +88,12 @@ public void can_observe_resource() throws InterruptedException { assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals("/3/0/15", observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() .getObservations(helper.getCurrentRegistration()); - assertTrue("We should have only on observation", observations.size() == 1); + assertTrue("We should have only one observation", observations.size() == 1); assertTrue("New observation is not there", observations.contains(observation)); // write device timezone @@ -103,9 +104,10 @@ public void can_observe_resource() throws InterruptedException { listener.waitForNotification(2000); assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); assertTrue(listener.receivedNotify().get()); - assertEquals(LwM2mSingleResource.newStringResource(15, "Europe/Paris"), listener.getResponse().getContent()); - assertNotNull(listener.getResponse().getCoapResponse()); - assertThat(listener.getResponse().getCoapResponse(), is(instanceOf(Response.class))); + assertEquals(LwM2mSingleResource.newStringResource(15, "Europe/Paris"), + (listener.getObserveResponse()).getContent()); + assertNotNull(listener.getObserveResponse().getCoapResponse()); + assertThat(listener.getObserveResponse().getCoapResponse(), is(instanceOf(Response.class))); } @Test @@ -122,7 +124,7 @@ public void can_observe_resource_instance() throws InterruptedException { assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals(expectedPath, observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() @@ -138,9 +140,10 @@ public void can_observe_resource_instance() throws InterruptedException { listener.waitForNotification(2000); assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); assertTrue(listener.receivedNotify().get()); - assertEquals(LwM2mResourceInstance.newStringInstance(0, "a new string"), listener.getResponse().getContent()); - assertNotNull(listener.getResponse().getCoapResponse()); - assertThat(listener.getResponse().getCoapResponse(), is(instanceOf(Response.class))); + assertEquals(LwM2mResourceInstance.newStringInstance(0, "a new string"), + listener.getObserveResponse().getContent()); + assertNotNull(listener.getObserveResponse().getCoapResponse()); + assertThat(listener.getObserveResponse().getCoapResponse(), is(instanceOf(Response.class))); } @Test @@ -157,7 +160,7 @@ public void can_observe_resource_instance_then_passive_cancel() throws Interrupt assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals(expectedPath, observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() @@ -173,8 +176,9 @@ public void can_observe_resource_instance_then_passive_cancel() throws Interrupt listener.waitForNotification(2000); assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); assertTrue(listener.receivedNotify().get()); - assertEquals(LwM2mResourceInstance.newStringInstance(0, "a new string"), listener.getResponse().getContent()); - assertNotNull(listener.getResponse().getCoapResponse()); + assertEquals(LwM2mResourceInstance.newStringInstance(0, "a new string"), + listener.getObserveResponse().getContent()); + assertNotNull(listener.getObserveResponse().getCoapResponse()); // cancel observation : passive way helper.server.getObservationService().cancelObservation(observation); @@ -206,7 +210,7 @@ public void can_observe_resource_instance_then_active_cancel() throws Interrupte assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals(expectedPath, observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() @@ -222,8 +226,9 @@ public void can_observe_resource_instance_then_active_cancel() throws Interrupte listener.waitForNotification(2000); assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); assertTrue(listener.receivedNotify().get()); - assertEquals(LwM2mResourceInstance.newStringInstance(0, "a new string"), listener.getResponse().getContent()); - assertNotNull(listener.getResponse().getCoapResponse()); + assertEquals(LwM2mResourceInstance.newStringInstance(0, "a new string"), + listener.getObserveResponse().getContent()); + assertNotNull(listener.getObserveResponse().getCoapResponse()); // cancel observation : active way CancelObservationResponse response = helper.server.send(helper.getCurrentRegistration(), @@ -261,12 +266,12 @@ public void can_observe_resource_then_passive_cancel() throws InterruptedExcepti assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals("/3/0/15", observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() .getObservations(helper.getCurrentRegistration()); - assertTrue("We should have only on observation", observations.size() == 1); + assertEquals("We should have only one observation", 1, observations.size()); assertTrue("New observation is not there", observations.contains(observation)); // write device timezone @@ -277,9 +282,10 @@ public void can_observe_resource_then_passive_cancel() throws InterruptedExcepti listener.waitForNotification(2000); assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); assertTrue(listener.receivedNotify().get()); - assertEquals(LwM2mSingleResource.newStringResource(15, "Europe/Paris"), listener.getResponse().getContent()); - assertNotNull(listener.getResponse().getCoapResponse()); - assertThat(listener.getResponse().getCoapResponse(), is(instanceOf(Response.class))); + assertEquals(LwM2mSingleResource.newStringResource(15, "Europe/Paris"), + listener.getObserveResponse().getContent()); + assertNotNull(listener.getObserveResponse().getCoapResponse()); + assertThat(listener.getObserveResponse().getCoapResponse(), is(instanceOf(Response.class))); // cancel observation : passive way helper.server.getObservationService().cancelObservation(observation); @@ -310,12 +316,12 @@ public void can_observe_resource_then_active_cancel() throws InterruptedExceptio assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals("/3/0/15", observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() .getObservations(helper.getCurrentRegistration()); - assertTrue("We should have only on observation", observations.size() == 1); + assertEquals("We should have only one observation", 1, observations.size()); assertTrue("New observation is not there", observations.contains(observation)); // write device timezone @@ -326,9 +332,10 @@ public void can_observe_resource_then_active_cancel() throws InterruptedExceptio listener.waitForNotification(2000); assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); assertTrue(listener.receivedNotify().get()); - assertEquals(LwM2mSingleResource.newStringResource(15, "Europe/Paris"), listener.getResponse().getContent()); - assertNotNull(listener.getResponse().getCoapResponse()); - assertThat(listener.getResponse().getCoapResponse(), is(instanceOf(Response.class))); + assertEquals(LwM2mSingleResource.newStringResource(15, "Europe/Paris"), + listener.getObserveResponse().getContent()); + assertNotNull(listener.getObserveResponse().getCoapResponse()); + assertThat(listener.getObserveResponse().getCoapResponse(), is(instanceOf(Response.class))); // cancel observation : active way CancelObservationResponse response = helper.server.send(helper.getCurrentRegistration(), @@ -339,7 +346,7 @@ public void can_observe_resource_then_active_cancel() throws InterruptedExceptio // active cancellation does not remove observation from store : it should be done manually using // ObservationService().cancelObservation(observation) observations = helper.server.getObservationService().getObservations(helper.getCurrentRegistration()); - assertTrue("We should have only on observation", observations.size() == 1); + assertEquals("We should have only one observation", 1, observations.size()); assertTrue("Observation should still be there", observations.contains(observation)); // write device timezone @@ -365,12 +372,12 @@ public void can_observe_instance() throws InterruptedException { assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals("/3/0", observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() .getObservations(helper.getCurrentRegistration()); - assertTrue("We should have only on observation", observations.size() == 1); + assertEquals("We should have only one observation", 1, observations.size()); assertTrue("New observation is not there", observations.contains(observation)); // write device timezone @@ -381,14 +388,14 @@ public void can_observe_instance() throws InterruptedException { listener.waitForNotification(2000); assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); assertTrue(listener.receivedNotify().get()); - assertTrue(listener.getResponse().getContent() instanceof LwM2mObjectInstance); - assertNotNull(listener.getResponse().getCoapResponse()); - assertThat(listener.getResponse().getCoapResponse(), is(instanceOf(Response.class))); + assertTrue(listener.getObserveResponse().getContent() instanceof LwM2mObjectInstance); + assertNotNull(listener.getObserveResponse().getCoapResponse()); + assertThat(listener.getObserveResponse().getCoapResponse(), is(instanceOf(Response.class))); // try to read the object instance for comparing ReadResponse readResp = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3, 0)); - assertEquals(readResp.getContent(), listener.getResponse().getContent()); + assertEquals(readResp.getContent(), listener.getObserveResponse().getContent()); } @Test @@ -403,12 +410,12 @@ public void can_observe_object() throws InterruptedException { assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals("/3", observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() .getObservations(helper.getCurrentRegistration()); - assertTrue("We should have only on observation", observations.size() == 1); + assertEquals("We should have only one observation", 1, observations.size()); assertTrue("New observation is not there", observations.contains(observation)); // write device timezone @@ -419,14 +426,14 @@ public void can_observe_object() throws InterruptedException { listener.waitForNotification(2000); assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); assertTrue(listener.receivedNotify().get()); - assertTrue(listener.getResponse().getContent() instanceof LwM2mObject); - assertNotNull(listener.getResponse().getCoapResponse()); - assertThat(listener.getResponse().getCoapResponse(), is(instanceOf(Response.class))); + assertTrue(listener.getObserveResponse().getContent() instanceof LwM2mObject); + assertNotNull(listener.getObserveResponse().getCoapResponse()); + assertThat(listener.getObserveResponse().getCoapResponse(), is(instanceOf(Response.class))); // try to read the object for comparing ReadResponse readResp = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3)); - assertEquals(readResp.getContent(), listener.getResponse().getContent()); + assertEquals(readResp.getContent(), listener.getObserveResponse().getContent()); } @Test @@ -442,12 +449,12 @@ public void can_handle_error_on_notification() throws InterruptedException { assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals("/3/0/15", observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() .getObservations(helper.getCurrentRegistration()); - assertTrue("We should have only on observation", observations.size() == 1); + assertEquals("We should have only one observation", 1, observations.size()); assertTrue("New observation is not there", observations.contains(observation)); // *** HACK send a notification with unsupported content format *** // diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java index d10092cdab..affa68c24e 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java @@ -38,6 +38,7 @@ import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; import org.eclipse.leshan.core.node.codec.LwM2mEncoder; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.core.response.ObserveResponse; @@ -55,9 +56,9 @@ public class ObserveTimeStampTest { @Parameters(name = "{0}") public static Collection contentFormats() { return Arrays.asList(new Object[][] { // - { ContentFormat.JSON }, // - { ContentFormat.SENML_JSON }, // - { ContentFormat.SENML_CBOR } }); + { ContentFormat.JSON }, // + { ContentFormat.SENML_JSON }, // + { ContentFormat.SENML_CBOR } }); } protected IntegrationTestHelper helper = new IntegrationTestHelper(); @@ -99,12 +100,12 @@ public void can_observe_timestamped_resource() throws InterruptedException { assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals("/3/0/15", observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() .getObservations(helper.getCurrentRegistration()); - assertTrue("We should have only on observation", observations.size() == 1); + assertTrue("We should have only one observation", observations.size() == 1); assertTrue("New observation is not there", observations.contains(observation)); // *** HACK send time-stamped notification as Leshan client does not support it *** // @@ -125,9 +126,9 @@ public void can_observe_timestamped_resource() throws InterruptedException { // verify result listener.waitForNotification(2000); assertTrue(listener.receivedNotify().get()); - assertEquals(mostRecentNode.getNode(), listener.getResponse().getContent()); - assertEquals(timestampedNodes, listener.getResponse().getTimestampedLwM2mNode()); - assertContentFormat(contentFormat, listener.getResponse()); + assertEquals(mostRecentNode.getNode(), listener.getObserveResponse().getContent()); + assertEquals(timestampedNodes, listener.getObserveResponse().getTimestampedLwM2mNode()); + assertContentFormat(contentFormat, listener.getObserveResponse()); } @Test @@ -142,12 +143,12 @@ public void can_observe_timestamped_instance() throws InterruptedException { assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals("/3/0", observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() .getObservations(helper.getCurrentRegistration()); - assertTrue("We should have only on observation", observations.size() == 1); + assertTrue("We should have only one observation", observations.size() == 1); assertTrue("New observation is not there", observations.contains(observation)); // *** HACK send time-stamped notification as Leshan client does not support it *** // @@ -168,9 +169,9 @@ public void can_observe_timestamped_instance() throws InterruptedException { // verify result listener.waitForNotification(2000); assertTrue(listener.receivedNotify().get()); - assertEquals(mostRecentNode.getNode(), listener.getResponse().getContent()); - assertEquals(timestampedNodes, listener.getResponse().getTimestampedLwM2mNode()); - assertContentFormat(contentFormat, listener.getResponse()); + assertEquals(mostRecentNode.getNode(), listener.getObserveResponse().getContent()); + assertEquals(timestampedNodes, listener.getObserveResponse().getTimestampedLwM2mNode()); + assertContentFormat(contentFormat, listener.getObserveResponse()); } @Test @@ -185,12 +186,12 @@ public void can_observe_timestamped_object() throws InterruptedException { assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals("/3", observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() .getObservations(helper.getCurrentRegistration()); - assertTrue("We should have only on observation", observations.size() == 1); + assertTrue("We should have only one observation", observations.size() == 1); assertTrue("New observation is not there", observations.contains(observation)); // *** HACK send time-stamped notification as Leshan client does not support it *** // @@ -212,8 +213,8 @@ public void can_observe_timestamped_object() throws InterruptedException { // verify result listener.waitForNotification(2000); assertTrue(listener.receivedNotify().get()); - assertEquals(mostRecentNode.getNode(), listener.getResponse().getContent()); - assertEquals(timestampedNodes, listener.getResponse().getTimestampedLwM2mNode()); - assertContentFormat(contentFormat, listener.getResponse()); + assertEquals(mostRecentNode.getNode(), listener.getObserveResponse().getContent()); + assertEquals(timestampedNodes, listener.getObserveResponse().getTimestampedLwM2mNode()); + assertContentFormat(contentFormat, listener.getObserveResponse()); } } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/TestObservationListener.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/TestObservationListener.java index 6b976515c4..75333b3397 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/TestObservationListener.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/TestObservationListener.java @@ -5,7 +5,10 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.server.observation.ObservationListener; import org.eclipse.leshan.server.registration.Registration; @@ -15,13 +18,24 @@ public class TestObservationListener implements ObservationListener { private CountDownLatch latch = new CountDownLatch(1); private final AtomicBoolean receivedNotify = new AtomicBoolean(); private AtomicInteger counter = new AtomicInteger(0); - private ObserveResponse response; + private ObserveResponse observeResponse; + private ObserveCompositeResponse observeCompositeResponse; private Exception error; @Override - public void onResponse(Observation observation, Registration registration, ObserveResponse response) { + public void onResponse(SingleObservation observation, Registration registration, ObserveResponse response) { receivedNotify.set(true); - this.response = response; + this.observeResponse = response; + this.error = null; + this.counter.incrementAndGet(); + latch.countDown(); + } + + @Override + public void onResponse(CompositeObservation observation, Registration registration, + ObserveCompositeResponse response) { + receivedNotify.set(true); + this.observeCompositeResponse = response; this.error = null; this.counter.incrementAndGet(); latch.countDown(); @@ -30,7 +44,8 @@ public void onResponse(Observation observation, Registration registration, Obser @Override public void onError(Observation observation, Registration registration, Exception error) { receivedNotify.set(true); - this.response = null; + this.observeResponse = null; + this.observeCompositeResponse = null; this.error = error; latch.countDown(); } @@ -48,8 +63,12 @@ public AtomicBoolean receivedNotify() { return receivedNotify; } - public ObserveResponse getResponse() { - return response; + public ObserveResponse getObserveResponse() { + return observeResponse; + } + + public ObserveCompositeResponse getObserveCompositeResponse() { + return observeCompositeResponse; } public Exception getError() { @@ -67,7 +86,8 @@ public int getNotificationCount() { public void reset() { latch = new CountDownLatch(1); receivedNotify.set(false); - response = null; + this.observeResponse = null; + this.observeCompositeResponse = null; error = null; this.counter = new AtomicInteger(0); } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/server/redis/RedisRegistrationStoreTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/server/redis/RedisRegistrationStoreTest.java new file mode 100644 index 0000000000..6c9625021a --- /dev/null +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/server/redis/RedisRegistrationStoreTest.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * Copyright (c) 2021 Orange. + * + * 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: + * Michał Wadowski (Orange) - Add Observe-Composite feature. + *******************************************************************************/ +package org.eclipse.leshan.integration.tests.server.redis; + +import static org.junit.Assert.*; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.Map; + +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Token; +import org.eclipse.californium.elements.AddressEndpointContext; +import org.eclipse.leshan.core.Link; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.request.BindingMode; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.integration.tests.util.RedisIntegrationTestHelper; +import org.eclipse.leshan.server.californium.observation.ObserveUtil; +import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; +import org.eclipse.leshan.server.redis.RedisRegistrationStore; +import org.eclipse.leshan.server.registration.Registration; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class RedisRegistrationStoreTest { + + private final String ep = "urn:endpoint"; + private final int port = 23452; + private final Long lifetime = 10000L; + private final String sms = "0171-32423545"; + private final EnumSet binding = EnumSet.of(BindingMode.U, BindingMode.Q, BindingMode.S); + private final Link[] objectLinks = Link.parse("".getBytes(StandardCharsets.UTF_8)); + private final String registrationId = "4711"; + private final Token exampleToken = Token.EMPTY; + + CaliforniumRegistrationStore store; + InetAddress address; + Registration registration; + + RedisIntegrationTestHelper helper; + + @Before + public void setUp() throws UnknownHostException { + helper = new RedisIntegrationTestHelper(); + address = InetAddress.getLocalHost(); + store = new RedisRegistrationStore(helper.createJedisPool()); + } + + @After + public void stop() { + store.removeRegistration(registrationId); + } + + @Test + public void get_observation_from_request() { + // given + String examplePath = "/1/2/3"; + + givenASimpleRegistration(lifetime); + store.addRegistration(registration); + + org.eclipse.californium.core.observe.Observation observationToStore = prepareCoapObservationOnSingle( + examplePath); + + // when + store.put(exampleToken, observationToStore); + + // then + Observation leshanObservation = store.getObservation(registrationId, exampleToken.getBytes()); + assertNotNull(leshanObservation); + assertTrue(leshanObservation instanceof SingleObservation); + SingleObservation observation = (SingleObservation) leshanObservation; + assertEquals(examplePath, observation.getPath().toString()); + } + + private void givenASimpleRegistration(Long lifetime) { + Registration.Builder builder = new Registration.Builder(registrationId, ep, Identity.unsecure(address, port)); + + registration = builder.lifeTimeInSec(lifetime).smsNumber(sms).bindingMode(binding).objectLinks(objectLinks) + .build(); + } + + private org.eclipse.californium.core.observe.Observation prepareCoapObservationOnSingle(String path) { + ObserveRequest observeRequest = new ObserveRequest(null, path); + + Map userContext = ObserveUtil.createCoapObserveRequestContext(ep, registrationId, + observeRequest); + + return prepareCoapObservation(new Request(CoAP.Code.GET), userContext); + } + + private org.eclipse.californium.core.observe.Observation prepareCoapObservation(Request coapRequest, + Map userContext) { + coapRequest.setUserContext(userContext); + coapRequest.setToken(exampleToken); + coapRequest.setObserve(); + coapRequest.getOptions().setAccept(ContentFormat.DEFAULT.getCode()); + coapRequest.setMID(1); + + coapRequest.setDestinationContext(new AddressEndpointContext(new InetSocketAddress(address, port))); + + return new org.eclipse.californium.core.observe.Observation(coapRequest, null); + } +} diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/RedisIntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/RedisIntegrationTestHelper.java index 7a5fc250a8..07fa2944f7 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/RedisIntegrationTestHelper.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/RedisIntegrationTestHelper.java @@ -41,10 +41,7 @@ public void createServer() { builder.setSecurityStore(new InMemorySecurityStore()); // Create redis store - String redisURI = System.getenv("REDIS_URI"); - if (redisURI == null) - redisURI = ""; - Pool jedis = new JedisPool(redisURI); + Pool jedis = createJedisPool(); builder.setRegistrationStore(new RedisRegistrationStore(jedis)); // Build server ! @@ -52,4 +49,11 @@ public void createServer() { // monitor client registration setupServerMonitoring(); } + + public Pool createJedisPool() { + String redisURI = System.getenv("REDIS_URI"); + if (redisURI == null) + redisURI = ""; + return new JedisPool(redisURI); + } } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteCompositeTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteCompositeTest.java index a8a46a1aee..53f1574563 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteCompositeTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteCompositeTest.java @@ -33,7 +33,7 @@ import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResourceInstance; import org.eclipse.leshan.core.node.LwM2mSingleResource; -import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.core.request.ReadRequest; @@ -178,7 +178,7 @@ public void can_observe_instance_with_composite_write() throws InterruptedExcept assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); // an observation response should have been sent - Observation observation = observeResponse.getObservation(); + SingleObservation observation = observeResponse.getObservation(); assertEquals("/3/0", observation.getPath().toString()); assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); @@ -194,11 +194,11 @@ public void can_observe_instance_with_composite_write() throws InterruptedExcept listener.waitForNotification(1000); assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); assertTrue(listener.receivedNotify().get()); - assertTrue(listener.getResponse().getContent() instanceof LwM2mObjectInstance); - assertNotNull(listener.getResponse().getCoapResponse()); - assertThat(listener.getResponse().getCoapResponse(), is(instanceOf(Response.class))); + assertTrue(listener.getObserveResponse().getContent() instanceof LwM2mObjectInstance); + assertNotNull(listener.getObserveResponse().getCoapResponse()); + assertThat(listener.getObserveResponse().getCoapResponse(), is(instanceOf(Response.class))); - LwM2mObjectInstance instance = (LwM2mObjectInstance) listener.getResponse().getContent(); + LwM2mObjectInstance instance = (LwM2mObjectInstance) listener.getObserveResponse().getContent(); assertEquals("+11", instance.getResource(14).getValue()); assertEquals("Moon", instance.getResource(15).getValue()); diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObservationServiceImpl.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObservationServiceImpl.java index 9d9d698b09..4b09a72113 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObservationServiceImpl.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObservationServiceImpl.java @@ -12,6 +12,7 @@ * * Contributors: * Sierra Wireless - initial API and implementation + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.server.californium.observation; @@ -31,16 +32,21 @@ import org.eclipse.californium.core.network.Endpoint; import org.eclipse.californium.core.observe.NotificationListener; import org.eclipse.californium.core.observe.ObservationStore; +import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.californium.EndpointContextUtil; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.node.codec.LwM2mDecoder; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.Identity; import org.eclipse.leshan.core.request.exception.InvalidResponseException; +import org.eclipse.leshan.core.response.AbstractLwM2mResponse; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.core.util.Hex; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; @@ -190,8 +196,10 @@ private Set getObservations(String registrationId, String resourceP Set result = new HashSet<>(); LwM2mPath lwPath = new LwM2mPath(resourcePath); for (Observation obs : getObservations(registrationId)) { - if (lwPath.equals(obs.getPath())) { - result.add(obs); + if (obs instanceof SingleObservation) { + if (lwPath.equals(((SingleObservation) obs).getPath())) { + result.add(obs); + } } } return result; @@ -261,11 +269,19 @@ public void onNotification(Request coapRequest, Response coapResponse) { LwM2mModel model = modelProvider.getObjectModel(registration); // create response - ObserveResponse response = createObserveResponse(observation, model, coapResponse); - - // notify all listeners - for (ObservationListener listener : listeners) { - listener.onResponse(observation, registration, response); + AbstractLwM2mResponse response = createObserveResponse(observation, model, coapResponse); + + if (response != null) { + // notify all listeners + for (ObservationListener listener : listeners) { + if (observation instanceof SingleObservation && response instanceof ObserveResponse) { + listener.onResponse((SingleObservation) observation, registration, (ObserveResponse) response); + } + if (observation instanceof CompositeObservation && response instanceof ObserveCompositeResponse) { + listener.onResponse((CompositeObservation) observation, registration, + (ObserveCompositeResponse) response); + } + } } } catch (InvalidResponseException e) { if (LOG.isDebugEnabled()) { @@ -284,10 +300,10 @@ public void onNotification(Request coapRequest, Response coapResponse) { listener.onError(observation, registration, e); } } - } - private ObserveResponse createObserveResponse(Observation observation, LwM2mModel model, Response coapResponse) { + private AbstractLwM2mResponse createObserveResponse(Observation observation, LwM2mModel model, + Response coapResponse) { // CHANGED response is supported for backward compatibility with old spec. if (coapResponse.getCode() != CoAP.ResponseCode.CHANGED && coapResponse.getCode() != CoAP.ResponseCode.CONTENT) { @@ -303,17 +319,25 @@ private ObserveResponse createObserveResponse(Observation observation, LwM2mMode // decode response try { - List timestampedNodes = decoder.decodeTimestampedData(coapResponse.getPayload(), - contentFormat, observation.getPath(), model); - - // create lwm2m response - if (timestampedNodes.size() == 1 && !timestampedNodes.get(0).isTimestamped()) { - return new ObserveResponse(toLwM2mResponseCode(coapResponse.getCode()), - timestampedNodes.get(0).getNode(), null, observation, null, coapResponse); - } else { - return new ObserveResponse(toLwM2mResponseCode(coapResponse.getCode()), null, timestampedNodes, - observation, null, coapResponse); + ResponseCode responseCode = toLwM2mResponseCode(coapResponse.getCode()); + + if (observation instanceof SingleObservation) { + SingleObservation singleObservation = (SingleObservation) observation; + + List timestampedNodes = decoder.decodeTimestampedData(coapResponse.getPayload(), + contentFormat, singleObservation.getPath(), model); + + // create lwm2m response + if (timestampedNodes.size() == 1 && !timestampedNodes.get(0).isTimestamped()) { + return new ObserveResponse(responseCode, timestampedNodes.get(0).getNode(), null, singleObservation, + null, coapResponse); + } else { + return new ObserveResponse(responseCode, null, timestampedNodes, singleObservation, null, + coapResponse); + } } + + return null; } catch (CodecException e) { if (LOG.isDebugEnabled()) { byte[] payload = coapResponse.getPayload() == null ? new byte[0] : coapResponse.getPayload(); diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java index d9b78d51c5..94e3cbcbce 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java @@ -12,16 +12,19 @@ * * Contributors: * Sierra Wireless - initial API and implementation + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.server.californium.observation; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.eclipse.californium.core.coap.Request; import org.eclipse.leshan.core.node.LwM2mPath; -import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; @@ -40,34 +43,57 @@ public class ObserveUtil { /** * Create a LWM2M observation from a CoAP request. */ - public static Observation createLwM2mObservation(Request request) { - String regId = null; - String lwm2mPath = null; - Map context = null; - - for (Entry ctx : request.getUserContext().entrySet()) { - switch (ctx.getKey()) { - case CTX_REGID: - regId = ctx.getValue(); - break; - case CTX_LWM2M_PATH: - lwm2mPath = ctx.getValue(); - break; - case CTX_ENDPOINT: - break; - default: - if (context == null) { - context = new HashMap<>(); + public static SingleObservation createLwM2mObservation(Request request) { + ObserveCommon observeCommon = new ObserveCommon(request); + + if (observeCommon.lwm2mPath.size() != 1) { + throw new IllegalStateException( + "1 path is expected in observe request context but was " + observeCommon.lwm2mPath); + } + + return new SingleObservation(request.getToken().getBytes(), observeCommon.regId, observeCommon.lwm2mPath.get(0), + observeCommon.contentFormat, observeCommon.context); + } + + private static class ObserveCommon { + String regId; + Map context; + List lwm2mPath; + ContentFormat contentFormat; + + public ObserveCommon(Request request) { + if (request.getUserContext() == null) { + throw new IllegalStateException("missing request context"); + } + + lwm2mPath = new ArrayList<>(); + context = new HashMap<>(); + + for (Entry ctx : request.getUserContext().entrySet()) { + switch (ctx.getKey()) { + case CTX_REGID: + regId = ctx.getValue(); + break; + case CTX_LWM2M_PATH: + for (String path : ctx.getValue().split("\n")) { + lwm2mPath.add(new LwM2mPath(path)); + } + break; + case CTX_ENDPOINT: + break; + default: + context.put(ctx.getKey(), ctx.getValue()); } - context.put(ctx.getKey(), ctx.getValue()); } - } - ContentFormat contentFormat = null; - if (request.getOptions().hasAccept()) { - contentFormat = ContentFormat.fromCode(request.getOptions().getAccept()); + if (lwm2mPath.size() == 0) { + throw new IllegalStateException("missing path in request context"); + } + + if (request.getOptions().hasAccept()) { + contentFormat = ContentFormat.fromCode(request.getOptions().getAccept()); + } } - return new Observation(request.getToken().getBytes(), regId, new LwM2mPath(lwm2mPath), contentFormat, context); } /** diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java index 45c4182418..d9374f8ead 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java @@ -21,6 +21,7 @@ * EndpointContext * Achim Kraus (Bosch Software Innovations GmbH) - update to modified * ObservationStore API + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.server.californium.registration; @@ -50,6 +51,7 @@ import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Stoppable; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.Identity; import org.eclipse.leshan.core.util.NamedThreadFactory; import org.eclipse.leshan.server.californium.observation.ObserveUtil; @@ -248,7 +250,7 @@ public Collection addObservation(String registrationId, Observation lock.writeLock().lock(); // cancel existing observations for the same path and registration id. for (Observation obs : unsafeGetObservations(registrationId)) { - if (observation.getPath().equals(obs.getPath()) && !Arrays.equals(observation.getId(), obs.getId())) { + if (areTheSamePaths(observation, obs) && !Arrays.equals(observation.getId(), obs.getId())) { unsafeRemoveObservation(new Token(obs.getId())); removed.add(obs); } @@ -260,6 +262,13 @@ public Collection addObservation(String registrationId, Observation return removed; } + private boolean areTheSamePaths(Observation observation, Observation obs) { + if (observation instanceof SingleObservation && obs instanceof SingleObservation) { + return ((SingleObservation) observation).getPath().equals(((SingleObservation) obs).getPath()); + } + return false; + } + @Override public Observation removeObservation(String registrationId, byte[] observationId) { try { diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java index 07bb59fe68..07e6a6b493 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java @@ -42,6 +42,7 @@ import org.eclipse.leshan.core.request.DownlinkRequestVisitor; import org.eclipse.leshan.core.request.ExecuteRequest; import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.core.request.ReadCompositeRequest; import org.eclipse.leshan.core.request.ReadRequest; @@ -192,6 +193,11 @@ public void visit(ReadCompositeRequest request) { applyLowerLayerConfig(coapRequest); } + @Override + public void visit(ObserveCompositeRequest request) { + throw new UnsupportedOperationException("Not implemented yet."); + } + @Override public void visit(WriteCompositeRequest request) { coapRequest = Request.newIPatch(); diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java index 0c35e2d615..4c386f0538 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java @@ -30,7 +30,7 @@ import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.node.codec.LwM2mDecoder; -import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.BootstrapDeleteRequest; import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; import org.eclipse.leshan.core.request.BootstrapFinishRequest; @@ -44,6 +44,7 @@ import org.eclipse.leshan.core.request.DownlinkRequestVisitor; import org.eclipse.leshan.core.request.ExecuteRequest; import org.eclipse.leshan.core.request.LwM2mRequest; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.core.request.ReadCompositeRequest; import org.eclipse.leshan.core.request.ReadRequest; @@ -229,7 +230,7 @@ public void visit(ObserveRequest request) { LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint); if (coapResponse.getOptions().hasObserve()) { // observe request successful - Observation observation = ObserveUtil.createLwM2mObservation(coapRequest); + SingleObservation observation = ObserveUtil.createLwM2mObservation(coapRequest); // add the observation to an ObserveResponse instance lwM2mresponse = new ObserveResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null, observation, null, coapResponse); @@ -279,6 +280,11 @@ public void visit(ReadCompositeRequest request) { } } + @Override + public void visit(ObserveCompositeRequest request) { + throw new UnsupportedOperationException("Not implemented yet."); + } + @Override public void visit(WriteCompositeRequest request) { if (coapResponse.isError()) { diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/DummyDecoder.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/DummyDecoder.java new file mode 100644 index 0000000000..dd6304b035 --- /dev/null +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/DummyDecoder.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2021 Orange. + * + * 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: + * Michał Wadowski (Orange) - Add Observe-Composite feature. + *******************************************************************************/ +package org.eclipse.leshan.server.californium; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.node.TimestampedLwM2mNode; +import org.eclipse.leshan.core.node.codec.CodecException; +import org.eclipse.leshan.core.node.codec.LwM2mDecoder; +import org.eclipse.leshan.core.request.ContentFormat; + +public class DummyDecoder implements LwM2mDecoder { + @Override + public LwM2mNode decode(byte[] content, ContentFormat format, LwM2mPath path, LwM2mModel model) + throws CodecException { + return LwM2mSingleResource.newResource(15, "Example"); + } + + @Override + public T decode(byte[] content, ContentFormat format, LwM2mPath path, LwM2mModel model, + Class nodeClass) throws CodecException { + return null; + } + + @Override + public Map decodeNodes(byte[] content, ContentFormat format, List paths, + LwM2mModel model) throws CodecException { + return null; + } + + @Override + public List decodeTimestampedData(byte[] content, ContentFormat format, LwM2mPath path, + LwM2mModel model) throws CodecException { + return Collections.singletonList(new TimestampedLwM2mNode(null, decode(null, null, null, null))); + } + + @Override + public List decodePaths(byte[] content, ContentFormat format) throws CodecException { + return null; + } + + @Override + public boolean isSupported(ContentFormat format) { + return false; + } + + @Override + public Set getSupportedContentFormat() { + return null; + } +} diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObservationServiceTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObservationServiceTest.java index 1101952f63..873a758074 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObservationServiceTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObservationServiceTest.java @@ -12,25 +12,36 @@ * * Contributors: * Sierra Wireless - initial API and implementation + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.server.californium.observation; +import static org.junit.Assert.assertNotNull; + import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Map; import java.util.Set; +import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Response; import org.eclipse.leshan.core.californium.EndpointContextUtil; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.Identity; import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; +import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.server.californium.CaliforniumTestSupport; +import org.eclipse.leshan.server.californium.DummyDecoder; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; import org.eclipse.leshan.server.californium.registration.InMemoryRegistrationStore; import org.eclipse.leshan.server.model.StandardModelProvider; +import org.eclipse.leshan.server.observation.ObservationListener; import org.eclipse.leshan.server.registration.Registration; import org.junit.Assert; import org.junit.Before; @@ -39,22 +50,21 @@ public class ObservationServiceTest { Request coapRequest; - LwM2mPath target; ObservationServiceImpl observationService; CaliforniumRegistrationStore store; - private CaliforniumTestSupport support = new CaliforniumTestSupport(); + private final CaliforniumTestSupport support = new CaliforniumTestSupport(); @Before public void setUp() throws Exception { support.givenASimpleClient(); store = new InMemoryRegistrationStore(); - observationService = new ObservationServiceImpl(store, new StandardModelProvider(), - new DefaultLwM2mDecoder()); } @Test public void observe_twice_cancels_first() { + createDefaultObservationService(); + givenAnObservation(support.registration.getId(), new LwM2mPath(3, 0, 12)); givenAnObservation(support.registration.getId(), new LwM2mPath(3, 0, 12)); @@ -65,6 +75,8 @@ public void observe_twice_cancels_first() { @Test public void cancel_by_client() { + createDefaultObservationService(); + // create some observations givenAnObservation(support.registration.getId(), new LwM2mPath(3, 0, 13)); givenAnObservation(support.registration.getId(), new LwM2mPath(3, 0, 12)); @@ -86,6 +98,8 @@ public void cancel_by_client() { @Test public void cancel_by_path() { + createDefaultObservationService(); + // create some observations givenAnObservation(support.registration.getId(), new LwM2mPath(3, 0, 13)); givenAnObservation(support.registration.getId(), new LwM2mPath(3, 0, 12)); @@ -107,7 +121,9 @@ public void cancel_by_path() { } @Test - public void cancel_by_observation() throws UnknownHostException { + public void cancel_by_observation() { + createDefaultObservationService(); + // create some observations givenAnObservation(support.registration.getId(), new LwM2mPath(3, 0, 13)); givenAnObservation(support.registration.getId(), new LwM2mPath(3, 0, 12)); @@ -127,6 +143,36 @@ public void cancel_by_observation() throws UnknownHostException { Assert.assertEquals(1, observations.size()); } + @Test + public void on_notification_observe_response() { + // given + createDummyDecoderObservationService(); + + givenAnObservation(support.registration.getId(), new LwM2mPath("/1/2/3")); + + Response coapResponse = new Response(CoAP.ResponseCode.CONTENT); + coapResponse.setToken(coapRequest.getToken()); + + CatchResponseObservationListener listener = new CatchResponseObservationListener(); + + observationService.addListener(listener); + + // when + observationService.onNotification(coapRequest, coapResponse); + + // then + assertNotNull(listener.observeResponse); + assertNotNull(listener.observation); + } + + private void createDummyDecoderObservationService() { + observationService = new ObservationServiceImpl(store, new StandardModelProvider(), new DummyDecoder()); + } + + private void createDefaultObservationService() { + observationService = new ObservationServiceImpl(store, new StandardModelProvider(), new DefaultLwM2mDecoder()); + } + private Observation givenAnObservation(String registrationId, LwM2mPath target) { Registration registration = store.getRegistration(registrationId); if (registration == null) { @@ -148,13 +194,13 @@ private Observation givenAnObservation(String registrationId, LwM2mPath target) store.put(coapRequest.getToken(), new org.eclipse.californium.core.observe.Observation(coapRequest, null)); - Observation observation = ObserveUtil.createLwM2mObservation(coapRequest); + SingleObservation observation = ObserveUtil.createLwM2mObservation(coapRequest); observationService.addObservation(registration, observation); return observation; } - public Registration givenASimpleClient(String registrationId) { + private Registration givenASimpleClient(String registrationId) { Registration.Builder builder; try { builder = new Registration.Builder(registrationId, registrationId + "_ep", @@ -164,4 +210,36 @@ public Registration givenASimpleClient(String registrationId) { throw new RuntimeException(e); } } + + private static class CatchResponseObservationListener implements ObservationListener { + + ObserveResponse observeResponse; + SingleObservation observation; + + @Override + public void newObservation(Observation observation, Registration registration) { + + } + + @Override + public void cancelled(Observation observation) { + + } + + @Override + public void onResponse(SingleObservation observation, Registration registration, ObserveResponse response) { + this.observeResponse = response; + this.observation = observation; + } + + @Override + public void onResponse(CompositeObservation observation, Registration registration, + ObserveCompositeResponse response) { + } + + @Override + public void onError(Observation observation, Registration registration, Exception error) { + + } + } } diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java new file mode 100644 index 0000000000..5b0959588c --- /dev/null +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2021 Orange. + * + * 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: + * Michał Wadowski (Orange) - Add Observe-Composite feature. + *******************************************************************************/ +package org.eclipse.leshan.server.californium.observation; + +import static org.junit.Assert.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Token; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.ObserveRequest; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; + +public class ObserveUtilTest { + + @Test + public void should_create_observation_from_context() { + // given + String examplePath = "/1/2/3"; + String exampleRegistrationId = "registrationId"; + Token exampleToken = Token.EMPTY; + + ObserveRequest observeRequest = new ObserveRequest(null, examplePath); + + // when + Map userContext = ObserveUtil.createCoapObserveRequestContext(null, exampleRegistrationId, + observeRequest); + userContext.put("extraKey", "extraValue"); + + Request coapRequest = new Request(null); + coapRequest.setUserContext(userContext); + coapRequest.setToken(exampleToken); + coapRequest.getOptions().setAccept(ContentFormat.DEFAULT.getCode()); + + SingleObservation observation = ObserveUtil.createLwM2mObservation(coapRequest); + + // then + assertEquals(examplePath, observation.getPath().toString()); + assertEquals(exampleRegistrationId, observation.getRegistrationId()); + assertEquals(exampleToken.getBytes(), observation.getId()); + assertTrue(observation.getContext().containsKey("extraKey")); + assertEquals("extraValue", observation.getContext().get("extraKey")); + assertEquals(ContentFormat.DEFAULT, observation.getContentFormat()); + } + + @Test + public void should_not_create_observation_without_context() { + // given + final Request coapRequest = new Request(null); + + // when / then + assertThrows(IllegalStateException.class, new ThrowingRunnable() { + @Override + public void run() { + ObserveUtil.createLwM2mObservation(coapRequest); + } + }); + } + + @Test + public void should_not_create_observation_without_path_in_context() { + // given + Map userContext = new HashMap<>(); + + final Request coapRequest = new Request(null); + coapRequest.setUserContext(userContext); + + // when / then + assertThrows(IllegalStateException.class, new ThrowingRunnable() { + @Override + public void run() { + ObserveUtil.createLwM2mObservation(coapRequest); + } + }); + } +} diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStoreTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStoreTest.java index 91ca39bdc2..4143b42e9f 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStoreTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStoreTest.java @@ -12,18 +12,30 @@ * * Contributors: * Sierra Wireless - initial API and implementation + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.server.californium.registration; +import static org.junit.Assert.*; + import java.net.InetAddress; +import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.EnumSet; +import java.util.Map; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Token; import org.eclipse.leshan.core.Link; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.BindingMode; +import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.server.californium.observation.ObserveUtil; import org.eclipse.leshan.server.registration.Registration; -import org.eclipse.leshan.server.registration.RegistrationStore; import org.eclipse.leshan.server.registration.RegistrationUpdate; import org.eclipse.leshan.server.registration.UpdatedRegistration; import org.junit.Assert; @@ -32,19 +44,22 @@ public class InMemoryRegistrationStoreTest { - RegistrationStore store; - String ep = "urn:endpoint"; + private final String ep = "urn:endpoint"; + private final int port = 23452; + private final Long lifetime = 10000L; + private final String sms = "0171-32423545"; + private final EnumSet binding = EnumSet.of(BindingMode.U, BindingMode.Q, BindingMode.S); + private final Link[] objectLinks = Link.parse("".getBytes(StandardCharsets.UTF_8)); + private final String registrationId = "4711"; + private final Token exampleToken = Token.EMPTY; + private final String examplePath = "/1/2/3"; + + CaliforniumRegistrationStore store; InetAddress address; - int port = 23452; - Long lifetime = 10000L; - String sms = "0171-32423545"; - EnumSet binding = EnumSet.of(BindingMode.U, BindingMode.Q, BindingMode.S); - Link[] objectLinks = Link.parse("".getBytes(StandardCharsets.UTF_8)); - String registrationId = "4711"; Registration registration; @Before - public void setUp() throws Exception { + public void setUp() throws UnknownHostException { address = InetAddress.getLocalHost(); store = new InMemoryRegistrationStore(); } @@ -57,16 +72,16 @@ public void update_registration_keeps_properties_unchanged() { RegistrationUpdate update = new RegistrationUpdate(registrationId, Identity.unsecure(address, port), null, null, null, null, null); UpdatedRegistration updatedRegistration = store.updateRegistration(update); - Assert.assertEquals(lifetime, updatedRegistration.getUpdatedRegistration().getLifeTimeInSec()); + assertEquals(lifetime, updatedRegistration.getUpdatedRegistration().getLifeTimeInSec()); Assert.assertSame(binding, updatedRegistration.getUpdatedRegistration().getBindingMode()); - Assert.assertEquals(sms, updatedRegistration.getUpdatedRegistration().getSmsNumber()); + assertEquals(sms, updatedRegistration.getUpdatedRegistration().getSmsNumber()); - Assert.assertEquals(registration, updatedRegistration.getPreviousRegistration()); + assertEquals(registration, updatedRegistration.getPreviousRegistration()); Registration reg = store.getRegistrationByEndpoint(ep); - Assert.assertEquals(lifetime, reg.getLifeTimeInSec()); + assertEquals(lifetime, reg.getLifeTimeInSec()); Assert.assertSame(binding, reg.getBindingMode()); - Assert.assertEquals(sms, reg.getSmsNumber()); + assertEquals(sms, reg.getSmsNumber()); } @Test @@ -91,6 +106,58 @@ public void update_registration_to_extend_time_to_live() { Assert.assertTrue(reg.isAlive()); } + @Test + public void put_coap_observation_with_valid_request() { + // given + givenASimpleRegistration(lifetime); + store.addRegistration(registration); + + org.eclipse.californium.core.observe.Observation observationToStore = prepareCoapObservation(); + + // when + store.put(exampleToken, observationToStore); + + // then + org.eclipse.californium.core.observe.Observation observationFetched = store.get(exampleToken); + + assertNotNull(observationFetched); + assertEquals(observationToStore.toString(), observationFetched.toString()); + } + + @Test + public void get_observation_from_request() { + // given + givenASimpleRegistration(lifetime); + store.addRegistration(registration); + + org.eclipse.californium.core.observe.Observation observationToStore = prepareCoapObservation(); + + // when + store.put(exampleToken, observationToStore); + + // then + Observation leshanObservation = store.getObservation(registrationId, exampleToken.getBytes()); + assertNotNull(leshanObservation); + assertTrue(leshanObservation instanceof SingleObservation); + SingleObservation observation = (SingleObservation) leshanObservation; + assertEquals(examplePath, observation.getPath().toString()); + } + + private org.eclipse.californium.core.observe.Observation prepareCoapObservation() { + ObserveRequest observeRequest = new ObserveRequest(null, examplePath); + + Map userContext = ObserveUtil.createCoapObserveRequestContext(ep, registrationId, + observeRequest); + + Request coapRequest = new Request(CoAP.Code.GET); + coapRequest.setUserContext(userContext); + coapRequest.setToken(exampleToken); + coapRequest.setObserve(); + coapRequest.getOptions().setAccept(ContentFormat.DEFAULT.getCode()); + + return new org.eclipse.californium.core.observe.Observation(coapRequest, null); + } + private void givenASimpleRegistration(Long lifetime) { Registration.Builder builder = new Registration.Builder(registrationId, ep, Identity.unsecure(address, port)); diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilderTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilderTest.java new file mode 100644 index 0000000000..e34e414a9d --- /dev/null +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilderTest.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) 2021 Orange. + * + * 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: + * Michał Wadowski (Orange) - Add Observe-Composite feature. + *******************************************************************************/ +package org.eclipse.leshan.server.californium.request; + +import static org.junit.Assert.*; + +import java.util.Map; + +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.coap.Token; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.response.ObserveResponse; +import org.eclipse.leshan.server.californium.DummyDecoder; +import org.eclipse.leshan.server.californium.observation.ObserveUtil; +import org.junit.Test; + +public class LwM2mResponseBuilderTest { + + @Test + public void visit_observe_request() { + // given + String examplePath = "/1/2/3"; + + ObserveRequest observeRequest = new ObserveRequest(null, examplePath); + + Map userContext = ObserveUtil.createCoapObserveRequestContext(null, null, observeRequest); + + Request coapRequest = new Request(null); + coapRequest.setToken(Token.EMPTY); + coapRequest.setUserContext(userContext); + + Response coapResponse = new Response(CoAP.ResponseCode.CONTENT); + coapResponse.getOptions().setObserve(1); + + LwM2mResponseBuilder responseBuilder = new LwM2mResponseBuilder<>(coapRequest, coapResponse, + null, null, new DummyDecoder()); + // when + responseBuilder.visit(observeRequest); + + // then + ObserveResponse response = responseBuilder.getResponse(); + assertNotNull(response); + assertNotNull(response.getObservation()); + + SingleObservation observation = response.getObservation(); + assertEquals(examplePath, observation.getPath().toString()); + } + +} diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationListener.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationListener.java index 5bb6a299f1..bdb3261277 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationListener.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationListener.java @@ -15,7 +15,10 @@ *******************************************************************************/ package org.eclipse.leshan.server.observation; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.server.registration.Registration; @@ -50,7 +53,17 @@ public interface ObservationListener { * @param response the lwm2m response received (successful or error response) * */ - void onResponse(Observation observation, Registration registration, ObserveResponse response); + void onResponse(SingleObservation observation, Registration registration, ObserveResponse response); + + /** + * Called on new notification. + * + * @param observation the composite-observation for which new data are received + * @param registration the registration concerned by this observation + * @param response the lwm2m observe-composite response received (successful or error response) + * + */ + void onResponse(CompositeObservation observation, Registration registration, ObserveCompositeResponse response); /** * Called when an error occurs on new notification. diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceStateListener.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceStateListener.java index e77dd148eb..49155b6c92 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceStateListener.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/PresenceStateListener.java @@ -17,7 +17,10 @@ import java.util.Collection; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.server.observation.ObservationListener; import org.eclipse.leshan.server.registration.Registration; @@ -66,7 +69,18 @@ public void unregistered(Registration reg, Collection observations, * @since 1.1 */ @Override - public void onResponse(Observation observation, Registration registration, ObserveResponse response) { + public void onResponse(SingleObservation observation, Registration registration, ObserveResponse response) { + presenceService.setAwake(registration); + } + + /** + * {@inheritDoc} + * + * @since 2.0 + */ + @Override + public void onResponse(CompositeObservation observation, Registration registration, + ObserveCompositeResponse response) { presenceService.setAwake(registration); } diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/EventServlet.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/EventServlet.java index d5db6be011..dd9310c90e 100644 --- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/EventServlet.java +++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/EventServlet.java @@ -12,12 +12,14 @@ * * Contributors: * Sierra Wireless - initial API and implementation + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.server.demo.servlet; import java.io.IOException; import java.util.Collection; import java.util.Collections; +import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -27,7 +29,11 @@ import org.eclipse.jetty.servlets.EventSource; import org.eclipse.jetty.servlets.EventSourceServlet; import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.server.californium.LeshanServer; import org.eclipse.leshan.server.demo.servlet.json.LwM2mNodeSerializer; @@ -127,16 +133,42 @@ public void cancelled(Observation observation) { } @Override - public void onResponse(Observation observation, Registration registration, ObserveResponse response) { + public void onResponse(SingleObservation observation, Registration registration, ObserveResponse response) { + String path = getObservationPaths(observation); + LwM2mNode content = response.getContent(); + String stringContent = content.toString(); + String jsonContent = gson.toJson(content); + + onResponseCommon(registration, path, stringContent, jsonContent); + } + + @Override + public void onResponse(CompositeObservation observation, Registration registration, + ObserveCompositeResponse response) { + String path = getObservationPaths(observation); + + Map content = response.getContent(); + String stringContent = content.toString(); + String jsonContent = gson.toJson(content); + + onResponseCommon(registration, path, stringContent, jsonContent); + } + + private void onResponseCommon(Registration registration, String path, String stringContent, + String jsonContent) { + if (LOG.isDebugEnabled()) { - LOG.debug("Received notification from [{}] containing value [{}]", observation.getPath(), - response.getContent().toString()); + LOG.debug("Received notification from [{}] containing value [{}]", path, stringContent); } if (registration != null) { - String data = new StringBuilder("{\"ep\":\"").append(registration.getEndpoint()).append("\",\"res\":\"") - .append(observation.getPath().toString()).append("\",\"val\":") - .append(gson.toJson(response.getContent())).append("}").toString(); + String data = new StringBuilder("{\"ep\":\"") + .append(registration.getEndpoint()) + .append("\",\"res\":\"") + .append(path).append("\",\"val\":") + .append(jsonContent) + .append("}") + .toString(); sendEvent(EVENT_NOTIFICATION, data, registration.getEndpoint()); } @@ -146,7 +178,7 @@ public void onResponse(Observation observation, Registration registration, Obser public void onError(Observation observation, Registration registration, Exception error) { if (LOG.isWarnEnabled()) { LOG.warn(String.format("Unable to handle notification of [%s:%s]", observation.getRegistrationId(), - observation.getPath()), error); + getObservationPaths(observation)), error); } } @@ -155,6 +187,16 @@ public void newObservation(Observation observation, Registration registration) { } }; + private String getObservationPaths(final Observation observation) { + String path = null; + if (observation instanceof SingleObservation) { + path = ((SingleObservation) observation).getPath().toString(); + } else if (observation instanceof CompositeObservation) { + path = ((CompositeObservation) observation).getPaths().toString(); + } + return path; + } + public EventServlet(LeshanServer server, int securePort) { server.getRegistrationService().addListener(this.registrationListener); server.getObservationService().addListener(this.observationListener); diff --git a/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java b/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java index 36f4d123d6..b5577dcda5 100644 --- a/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java +++ b/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java @@ -42,6 +42,7 @@ import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Stoppable; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.Identity; import org.eclipse.leshan.core.util.NamedThreadFactory; import org.eclipse.leshan.core.util.Validate; @@ -504,8 +505,7 @@ public Collection addObservation(String registrationId, Observation // cancel existing observations for the same path and registration id. for (Observation obs : getObservations(j, registrationId)) { - if (observation.getPath().equals(obs.getPath()) - && !Arrays.equals(observation.getId(), obs.getId())) { + if (areTheSamePaths(observation, obs) && !Arrays.equals(observation.getId(), obs.getId())) { removed.add(obs); unsafeRemoveObservation(j, registrationId, obs.getId()); } @@ -518,6 +518,13 @@ public Collection addObservation(String registrationId, Observation return removed; } + private boolean areTheSamePaths(Observation observation, Observation obs) { + if (observation instanceof SingleObservation && obs instanceof SingleObservation) { + return ((SingleObservation) observation).getPath().equals(((SingleObservation) obs).getPath()); + } + return false; + } + @Override public Observation removeObservation(String registrationId, byte[] observationId) { try (Jedis j = pool.getResource()) { diff --git a/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDesTest.java b/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDesTest.java index 86b8f6c177..5569470263 100644 --- a/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDesTest.java +++ b/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDesTest.java @@ -31,7 +31,7 @@ public class RegistrationSerDesTest { @Test - public void ser_and_des_are_equals() throws Exception { + public void ser_and_des_are_equals() { Link[] objs = new Link[2]; Map att = new HashMap<>(); att.put("ts", 12); @@ -55,7 +55,7 @@ public void ser_and_des_are_equals() throws Exception { } @Test - public void ser_and_des_are_equals_with_app_data() throws Exception { + public void ser_and_des_are_equals_with_app_data() { Link[] objs = new Link[2]; Map att = new HashMap<>(); att.put("ts", 12); diff --git a/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/SecurityInfoSerDesTest.java b/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/SecurityInfoSerDesTest.java index aca37ebfbd..2ba2e30599 100644 --- a/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/SecurityInfoSerDesTest.java +++ b/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/SecurityInfoSerDesTest.java @@ -27,7 +27,6 @@ import java.security.spec.KeySpec; import org.eclipse.leshan.core.util.Hex; -import org.eclipse.leshan.server.redis.serialization.SecurityInfoSerDes; import org.eclipse.leshan.server.security.SecurityInfo; import org.junit.Test; From 2ba58119f6ee0d2183dabccd8a581690bdbd8024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wadowski?= Date: Thu, 29 Jul 2021 11:46:29 +0200 Subject: [PATCH 2/8] Add server side Observe Composite feature --- .../tests/observe/ObserveTest.java | 2 +- .../observation/ObservationServiceImpl.java | 15 +++- .../californium/observation/ObserveUtil.java | 56 +++++++++++++- .../InMemoryRegistrationStore.java | 13 +++- .../CaliforniumLwM2mRequestSender.java | 16 +++- .../request/CoapRequestBuilder.java | 17 ++++- .../request/LwM2mResponseBuilder.java | 74 +++++++++++++------ .../observation/ObservationServiceTest.java | 61 ++++++++++++++- .../observation/ObserveUtilTest.java | 35 +++++++++ .../InMemoryRegistrationStoreTest.java | 40 ++++++++++ .../request/LwM2mResponseBuilderTest.java | 37 ++++++++++ .../server/redis/RedisRegistrationStore.java | 14 +++- 12 files changed, 343 insertions(+), 37 deletions(-) diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java index 3b150024bb..c045f315e4 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java @@ -93,7 +93,7 @@ public void can_observe_resource() throws InterruptedException { assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); Set observations = helper.server.getObservationService() .getObservations(helper.getCurrentRegistration()); - assertTrue("We should have only one observation", observations.size() == 1); + assertEquals("We should have only one observation", 1, observations.size()); assertTrue("New observation is not there", observations.contains(observation)); // write device timezone diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObservationServiceImpl.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObservationServiceImpl.java index 4b09a72113..a1de3f1d45 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObservationServiceImpl.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObservationServiceImpl.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; @@ -35,6 +36,7 @@ import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.californium.EndpointContextUtil; import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; import org.eclipse.leshan.core.node.codec.CodecException; @@ -335,9 +337,20 @@ private AbstractLwM2mResponse createObserveResponse(Observation observation, LwM return new ObserveResponse(responseCode, null, timestampedNodes, singleObservation, null, coapResponse); } + } else if (observation instanceof CompositeObservation) { + + CompositeObservation compositeObservation = (CompositeObservation) observation; + + Map nodes = decoder.decodeNodes(coapResponse.getPayload(), contentFormat, + compositeObservation.getPaths(), model); + + return new ObserveCompositeResponse(responseCode, nodes, null, coapResponse, compositeObservation); } - return null; + throw new IllegalStateException( + "observation must be a CompositeObservation or a SingleObservation but was " + observation == null + ? null + : observation.getClass().getSimpleName()); } catch (CodecException e) { if (LOG.isDebugEnabled()) { byte[] payload = coapResponse.getPayload() == null ? new byte[0] : coapResponse.getPayload(); diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java index 94e3cbcbce..9da454ca0b 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java @@ -22,10 +22,13 @@ import java.util.Map; import java.util.Map.Entry; +import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; @@ -55,6 +58,13 @@ public static SingleObservation createLwM2mObservation(Request request) { observeCommon.contentFormat, observeCommon.context); } + public static CompositeObservation createLwM2mCompositeObservation(Request request) { + ObserveCommon observeCommon = new ObserveCommon(request); + + return new CompositeObservation(request.getToken().getBytes(), observeCommon.regId, observeCommon.lwm2mPath, + observeCommon.contentFormat, observeCommon.context); + } + private static class ObserveCommon { String regId; Map context; @@ -111,12 +121,54 @@ public static Map createCoapObserveRequestContext(String endpoin return context; } + public static Map createCoapObserveCompositeRequestContext(String endpoint, String registrationId, + ObserveCompositeRequest request) { + Map context = new HashMap<>(); + context.put(CTX_ENDPOINT, endpoint); + context.put(CTX_REGID, registrationId); + + StringBuilder sb = new StringBuilder(); + for (LwM2mPath path : request.getPaths()) { + sb.append(path.toString()); + sb.append("\n"); + } + + context.put(CTX_LWM2M_PATH, sb.toString()); + for (Entry ctx : request.getContext().entrySet()) { + context.put(ctx.getKey(), ctx.getValue()); + } + return context; + } + public static String extractRegistrationId(org.eclipse.californium.core.observe.Observation observation) { return observation.getRequest().getUserContext().get(CTX_REGID); } - public static String extractLwm2mPath(org.eclipse.californium.core.observe.Observation observation) { - return observation.getRequest().getUserContext().get(CTX_LWM2M_PATH); + public static LwM2mPath extractLwm2mPath(org.eclipse.californium.core.observe.Observation observation) { + if (observation.getRequest().getCode() == CoAP.Code.GET) { + return new LwM2mPath(observation.getRequest().getUserContext().get(CTX_LWM2M_PATH)); + } else { + throw new IllegalStateException( + "Observation targeting only ont path must be a GET but was " + observation.getRequest().getCode()); + } + } + + public static List extractLwm2mPaths(org.eclipse.californium.core.observe.Observation observation) { + if (observation.getRequest().getCode() == CoAP.Code.FETCH) { + List lwm2mPath = new ArrayList<>(); + String pathsAsString = observation.getRequest().getUserContext().get(CTX_LWM2M_PATH); + for (String path : pathsAsString.split("\n")) { + lwm2mPath.add(new LwM2mPath(path)); + } + + if (lwm2mPath.size() == 0) { + throw new IllegalStateException("missing paths in request context"); + } + return lwm2mPath; + } else { + throw new IllegalStateException( + "Observation targeting several path must be a FETCH but was " + observation.getRequest().getCode()); + } } public static String extractEndpoint(org.eclipse.californium.core.observe.Observation observation) { diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java index d9374f8ead..9e1177633a 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java @@ -43,6 +43,7 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Token; import org.eclipse.californium.core.observe.ObservationStoreException; import org.eclipse.californium.core.observe.ObservationUtil; @@ -50,6 +51,7 @@ import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Stoppable; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.Identity; @@ -266,6 +268,9 @@ private boolean areTheSamePaths(Observation observation, Observation obs) { if (observation instanceof SingleObservation && obs instanceof SingleObservation) { return ((SingleObservation) observation).getPath().equals(((SingleObservation) obs).getPath()); } + if (observation instanceof CompositeObservation && obs instanceof CompositeObservation) { + return ((CompositeObservation) observation).getPaths().equals(((CompositeObservation) obs).getPaths()); + } return false; } @@ -455,7 +460,13 @@ private Observation build(org.eclipse.californium.core.observe.Observation cfObs if (cfObs == null) return null; - return ObserveUtil.createLwM2mObservation(cfObs.getRequest()); + if (cfObs.getRequest().getCode() == CoAP.Code.GET) { + return ObserveUtil.createLwM2mObservation(cfObs.getRequest()); + } else if (cfObs.getRequest().getCode() == CoAP.Code.FETCH) { + return ObserveUtil.createLwM2mCompositeObservation(cfObs.getRequest()); + } else { + throw new IllegalStateException("Observation request can be GET or FETCH only"); + } } private String validateObservation(org.eclipse.californium.core.observe.Observation observation) diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumLwM2mRequestSender.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumLwM2mRequestSender.java index 5bd03b9cea..53e1d97faa 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumLwM2mRequestSender.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumLwM2mRequestSender.java @@ -19,12 +19,14 @@ import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.network.Endpoint; +import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.californium.CoapResponseCallback; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.node.codec.LwM2mDecoder; import org.eclipse.leshan.core.node.codec.LwM2mEncoder; +import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.request.DownlinkRequest; import org.eclipse.leshan.core.request.exception.InvalidResponseException; import org.eclipse.leshan.core.request.exception.RequestCanceledException; @@ -34,10 +36,10 @@ import org.eclipse.leshan.core.request.exception.UnconnectedPeerException; import org.eclipse.leshan.core.response.ErrorCallback; import org.eclipse.leshan.core.response.LwM2mResponse; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.core.response.ResponseCallback; import org.eclipse.leshan.core.util.Validate; -import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.server.californium.observation.ObservationServiceImpl; import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.eclipse.leshan.server.registration.Registration; @@ -107,8 +109,16 @@ public T send(Registration destination, DownlinkReques destination.canInitiateConnection()); // Handle special observe case - if (response != null && response.getClass() == ObserveResponse.class && response.isSuccess()) { - observationService.addObservation(destination, ((ObserveResponse) response).getObservation()); + if (response != null && response.isSuccess()) { + Observation observation = null; + if (response instanceof ObserveResponse) { + observation = ((ObserveResponse) response).getObservation(); + } else if (response instanceof ObserveCompositeResponse) { + observation = ((ObserveCompositeResponse) response).getObservation(); + } + if (observation != null) { + observationService.addObservation(destination, observation); + } } return response; } diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java index 07e6a6b493..4994b3f939 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java @@ -15,6 +15,7 @@ * Achim Kraus (Bosch Software Innovations GmbH) - use Identity as destination * and transform them to * EndpointContext for requests + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.server.californium.request; @@ -195,7 +196,21 @@ public void visit(ReadCompositeRequest request) { @Override public void visit(ObserveCompositeRequest request) { - throw new UnsupportedOperationException("Not implemented yet."); + coapRequest = Request.newFetch(); + + coapRequest.getOptions().setContentFormat(request.getRequestContentFormat().getCode()); + + coapRequest.setPayload(encoder.encodePaths(request.getPaths(), request.getRequestContentFormat())); + + if (request.getResponseContentFormat() != null) { + coapRequest.getOptions().setAccept(request.getResponseContentFormat().getCode()); + } + + coapRequest.setObserve(); + setTarget(coapRequest, LwM2mPath.ROOTPATH); + + coapRequest.setUserContext(ObserveUtil.createCoapObserveCompositeRequestContext(endpoint, registrationId, request)); + applyLowerLayerConfig(coapRequest); } @Override diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java index 4c386f0538..c0ca882800 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java @@ -12,6 +12,7 @@ * * Contributors: * Sierra Wireless - initial API and implementation + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.server.californium.request; @@ -30,6 +31,7 @@ import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.node.codec.LwM2mDecoder; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.BootstrapDeleteRequest; import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; @@ -63,6 +65,7 @@ import org.eclipse.leshan.core.response.DiscoverResponse; import org.eclipse.leshan.core.response.ExecuteResponse; import org.eclipse.leshan.core.response.LwM2mResponse; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.core.response.ReadCompositeResponse; import org.eclipse.leshan.core.response.ReadResponse; @@ -107,7 +110,7 @@ public void visit(ReadRequest request) { // handle error response: lwM2mresponse = new ReadResponse(toLwM2mResponseCode(coapResponse.getCode()), null, coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT) { + } else if (isResponseCodeContent()) { // handle success response: LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint); lwM2mresponse = new ReadResponse(ResponseCode.CONTENT, content, null, coapResponse); @@ -123,7 +126,7 @@ public void visit(DiscoverRequest request) { // handle error response: lwM2mresponse = new DiscoverResponse(toLwM2mResponseCode(coapResponse.getCode()), null, coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT) { + } else if (isResponseCodeContent()) { // handle success response: Link[] links; if (MediaTypeRegistry.APPLICATION_LINK_FORMAT != coapResponse.getOptions().getContentFormat()) { @@ -145,7 +148,7 @@ public void visit(WriteRequest request) { // handle error response: lwM2mresponse = new WriteResponse(toLwM2mResponseCode(coapResponse.getCode()), coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED) { + } else if (isResponseCodeChanged()) { // handle success response: lwM2mresponse = new WriteResponse(ResponseCode.CHANGED, null, coapResponse); } else { @@ -160,7 +163,7 @@ public void visit(WriteAttributesRequest request) { // handle error response: lwM2mresponse = new WriteAttributesResponse(toLwM2mResponseCode(coapResponse.getCode()), coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED) { + } else if (isResponseCodeChanged()) { // handle success response: lwM2mresponse = new WriteAttributesResponse(ResponseCode.CHANGED, null, coapResponse); } else { @@ -175,7 +178,7 @@ public void visit(ExecuteRequest request) { // handle error response: lwM2mresponse = new ExecuteResponse(toLwM2mResponseCode(coapResponse.getCode()), coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED) { + } else if (isResponseCodeChanged()) { // handle success response: lwM2mresponse = new ExecuteResponse(ResponseCode.CHANGED, null, coapResponse); } else { @@ -223,21 +226,18 @@ public void visit(ObserveRequest request) { // handle error response: lwM2mresponse = new ObserveResponse(toLwM2mResponseCode(coapResponse.getCode()), null, null, null, coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT + } else if (isResponseCodeContent() // This is for backward compatibility, when the spec say notification used CHANGED code - || coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED) { + || isResponseCodeChanged()) { // handle success response: LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint); + SingleObservation observation = null; if (coapResponse.getOptions().hasObserve()) { // observe request successful - SingleObservation observation = ObserveUtil.createLwM2mObservation(coapRequest); - // add the observation to an ObserveResponse instance - lwM2mresponse = new ObserveResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null, - observation, null, coapResponse); - } else { - lwM2mresponse = new ObserveResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null, null, - null, coapResponse); + observation = ObserveUtil.createLwM2mObservation(coapRequest); } + lwM2mresponse = new ObserveResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null, observation, + null, coapResponse); } else { // handle unexpected response: handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); @@ -250,9 +250,9 @@ public void visit(CancelObservationRequest request) { // handle error response: lwM2mresponse = new CancelObservationResponse(toLwM2mResponseCode(coapResponse.getCode()), null, null, null, coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT + } else if (isResponseCodeContent() // This is for backward compatibility, when the spec say notification used CHANGED code - || coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED) { + || isResponseCodeChanged()) { // handle success response: LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint); lwM2mresponse = new CancelObservationResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null, @@ -269,7 +269,7 @@ public void visit(ReadCompositeRequest request) { // handle error response: lwM2mresponse = new ReadCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), null, coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT) { + } else if (isResponseCodeContent()) { // handle success response: Map content = decodeCompositeCoapResponse(request.getPaths(), coapResponse, request, clientEndpoint); @@ -282,7 +282,27 @@ public void visit(ReadCompositeRequest request) { @Override public void visit(ObserveCompositeRequest request) { - throw new UnsupportedOperationException("Not implemented yet."); + if (coapResponse.isError()) { + // handle error response: + lwM2mresponse = new ObserveCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), null, + coapResponse.getPayloadString(), coapResponse, null); + + } else if (isResponseCodeContent()) { + // handle success response: + Map content = decodeCompositeCoapResponse(request.getPaths(), coapResponse, request, + clientEndpoint); + + CompositeObservation observation = null; + if (coapResponse.getOptions().hasObserve()) { + // observe request successful + observation = ObserveUtil.createLwM2mCompositeObservation(coapRequest); + } + lwM2mresponse = new ObserveCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null, + coapResponse, observation); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } } @Override @@ -291,7 +311,7 @@ public void visit(WriteCompositeRequest request) { // handle error response: lwM2mresponse = new WriteCompositeResponse(toLwM2mResponseCode(coapResponse.getCode()), coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED) { + } else if (isResponseCodeChanged()) { // handle success response: lwM2mresponse = new WriteCompositeResponse(ResponseCode.CHANGED, null, coapResponse); } else { @@ -306,7 +326,7 @@ public void visit(BootstrapDiscoverRequest request) { // handle error response: lwM2mresponse = new BootstrapDiscoverResponse(toLwM2mResponseCode(coapResponse.getCode()), null, coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT) { + } else if (isResponseCodeContent()) { // handle success response: Link[] links; if (MediaTypeRegistry.APPLICATION_LINK_FORMAT != coapResponse.getOptions().getContentFormat()) { @@ -328,7 +348,7 @@ public void visit(BootstrapWriteRequest request) { // handle error response: lwM2mresponse = new BootstrapWriteResponse(toLwM2mResponseCode(coapResponse.getCode()), coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED) { + } else if (isResponseCodeChanged()) { // handle success response: lwM2mresponse = new BootstrapWriteResponse(ResponseCode.CHANGED, null, coapResponse); } else { @@ -343,7 +363,7 @@ public void visit(BootstrapReadRequest request) { // handle error response: lwM2mresponse = new BootstrapReadResponse(toLwM2mResponseCode(coapResponse.getCode()), null, coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT) { + } else if (isResponseCodeContent()) { // handle success response: LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint); lwM2mresponse = new BootstrapReadResponse(ResponseCode.CONTENT, content, null, coapResponse); @@ -374,7 +394,7 @@ public void visit(BootstrapFinishRequest request) { // handle error response: lwM2mresponse = new BootstrapFinishResponse(toLwM2mResponseCode(coapResponse.getCode()), coapResponse.getPayloadString(), coapResponse); - } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED) { + } else if (isResponseCodeChanged()) { // handle success response: lwM2mresponse = new BootstrapFinishResponse(ResponseCode.CHANGED, null, coapResponse); } else { @@ -383,6 +403,14 @@ public void visit(BootstrapFinishRequest request) { } } + private boolean isResponseCodeContent() { + return coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT; + } + + private boolean isResponseCodeChanged() { + return coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED; + } + private LwM2mNode decodeCoapResponse(LwM2mPath path, Response coapResponse, LwM2mRequest request, String endpoint) { diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObservationServiceTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObservationServiceTest.java index 873a758074..6666651a41 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObservationServiceTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObservationServiceTest.java @@ -16,7 +16,7 @@ *******************************************************************************/ package org.eclipse.leshan.server.californium.observation; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; import java.net.InetAddress; import java.net.UnknownHostException; @@ -33,7 +33,9 @@ import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.response.AbstractLwM2mResponse; import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.server.californium.CaliforniumTestSupport; @@ -93,7 +95,7 @@ public void cancel_by_client() { // check its absence observations = observationService.getObservations(support.registration); - Assert.assertTrue(observations.isEmpty()); + assertTrue(observations.isEmpty()); } @Test @@ -163,6 +165,32 @@ public void on_notification_observe_response() { // then assertNotNull(listener.observeResponse); assertNotNull(listener.observation); + assertTrue(listener.observeResponse instanceof ObserveResponse); + assertTrue(listener.observation instanceof SingleObservation); + } + + @Test + public void on_notification_composite_observe_response() { + // given + createDummyDecoderObservationService(); + + givenAnCompositeObservation(support.registration.getId(), new LwM2mPath("/1/2/3")); + + Response coapResponse = new Response(CoAP.ResponseCode.CONTENT); + coapResponse.setToken(coapRequest.getToken()); + + CatchResponseObservationListener listener = new CatchResponseObservationListener(); + + observationService.addListener(listener); + + // when + observationService.onNotification(coapRequest, coapResponse); + + // then + assertNotNull(listener.observeResponse); + assertNotNull(listener.observation); + assertTrue(listener.observeResponse instanceof ObserveCompositeResponse); + assertTrue(listener.observation instanceof CompositeObservation); } private void createDummyDecoderObservationService() { @@ -200,6 +228,29 @@ private Observation givenAnObservation(String registrationId, LwM2mPath target) return observation; } + private Observation givenAnCompositeObservation(String registrationId, LwM2mPath target) { + Registration registration = store.getRegistration(registrationId); + if (registration == null) { + registration = givenASimpleClient(registrationId); + store.addRegistration(registration); + } + + coapRequest = Request.newFetch(); + coapRequest.setToken(CaliforniumTestSupport.createToken()); + coapRequest.setObserve(); + coapRequest + .setDestinationContext(EndpointContextUtil.extractContext(support.registration.getIdentity(), false)); + Map context = ObserveUtil.createCoapObserveCompositeRequestContext(registration.getEndpoint(), + registrationId, new ObserveCompositeRequest(null, null, target.toString())); + coapRequest.setUserContext(context); + + store.put(coapRequest.getToken(), new org.eclipse.californium.core.observe.Observation(coapRequest, null)); + + CompositeObservation observation = ObserveUtil.createLwM2mCompositeObservation(coapRequest); + + return observation; + } + private Registration givenASimpleClient(String registrationId) { Registration.Builder builder; try { @@ -213,8 +264,8 @@ private Registration givenASimpleClient(String registrationId) { private static class CatchResponseObservationListener implements ObservationListener { - ObserveResponse observeResponse; - SingleObservation observation; + AbstractLwM2mResponse observeResponse; + Observation observation; @Override public void newObservation(Observation observation, Registration registration) { @@ -235,6 +286,8 @@ public void onResponse(SingleObservation observation, Registration registration, @Override public void onResponse(CompositeObservation observation, Registration registration, ObserveCompositeResponse response) { + this.observeResponse = response; + this.observation = observation; } @Override diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java index 5b0959588c..ad2f363d63 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java @@ -17,13 +17,18 @@ import static org.junit.Assert.*; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Token; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ObserveRequest; import org.junit.Test; import org.junit.function.ThrowingRunnable; @@ -60,6 +65,36 @@ public void should_create_observation_from_context() { assertEquals(ContentFormat.DEFAULT, observation.getContentFormat()); } + @Test + public void should_create_composite_observation_from_context() { + // given + List examplePaths = Arrays.asList(new LwM2mPath("/1/2/3"), new LwM2mPath("/4/5/6")); + String exampleRegistrationId = "registrationId"; + Token exampleToken = Token.EMPTY; + + ObserveCompositeRequest observeRequest = new ObserveCompositeRequest(null, null, examplePaths); + + // when + Map userContext = ObserveUtil.createCoapObserveCompositeRequestContext(null, + exampleRegistrationId, observeRequest); + userContext.put("extraKey", "extraValue"); + + Request coapRequest = new Request(null); + coapRequest.setUserContext(userContext); + coapRequest.setToken(exampleToken); + coapRequest.getOptions().setAccept(ContentFormat.DEFAULT.getCode()); + + CompositeObservation observation = ObserveUtil.createLwM2mCompositeObservation(coapRequest); + + // then + assertEquals(examplePaths, observation.getPaths()); + assertEquals(exampleRegistrationId, observation.getRegistrationId()); + assertEquals(exampleToken.getBytes(), observation.getId()); + assertTrue(observation.getContext().containsKey("extraKey")); + assertEquals("extraValue", observation.getContext().get("extraKey")); + assertEquals(ContentFormat.DEFAULT, observation.getContentFormat()); + } + @Test public void should_not_create_observation_without_context() { // given diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStoreTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStoreTest.java index 4143b42e9f..ee69e48f72 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStoreTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStoreTest.java @@ -21,18 +21,23 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.EnumSet; +import java.util.List; import java.util.Map; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Token; import org.eclipse.leshan.core.Link; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.BindingMode; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.server.californium.observation.ObserveUtil; import org.eclipse.leshan.server.registration.Registration; @@ -53,6 +58,7 @@ public class InMemoryRegistrationStoreTest { private final String registrationId = "4711"; private final Token exampleToken = Token.EMPTY; private final String examplePath = "/1/2/3"; + private final List examplePaths = Arrays.asList(new LwM2mPath("/1/2/3"), new LwM2mPath("/4/5/6")); CaliforniumRegistrationStore store; InetAddress address; @@ -143,6 +149,25 @@ public void get_observation_from_request() { assertEquals(examplePath, observation.getPath().toString()); } + @Test + public void get_composite_observation_from_request() { + // given + givenASimpleRegistration(lifetime); + store.addRegistration(registration); + + org.eclipse.californium.core.observe.Observation observationToStore = prepareCoapCompositeObservation(); + + // when + store.put(exampleToken, observationToStore); + + // then + Observation leshanObservation = store.getObservation(registrationId, exampleToken.getBytes()); + assertNotNull(leshanObservation); + assertTrue(leshanObservation instanceof CompositeObservation); + CompositeObservation observation = (CompositeObservation) leshanObservation; + assertEquals(examplePaths, observation.getPaths()); + } + private org.eclipse.californium.core.observe.Observation prepareCoapObservation() { ObserveRequest observeRequest = new ObserveRequest(null, examplePath); @@ -158,6 +183,21 @@ private org.eclipse.californium.core.observe.Observation prepareCoapObservation( return new org.eclipse.californium.core.observe.Observation(coapRequest, null); } + private org.eclipse.californium.core.observe.Observation prepareCoapCompositeObservation() { + ObserveCompositeRequest observeRequest = new ObserveCompositeRequest(null, null, examplePaths); + + Map userContext = ObserveUtil.createCoapObserveCompositeRequestContext(ep, registrationId, + observeRequest); + + Request coapRequest = new Request(CoAP.Code.FETCH); + coapRequest.setUserContext(userContext); + coapRequest.setToken(exampleToken); + coapRequest.setObserve(); + coapRequest.getOptions().setAccept(ContentFormat.DEFAULT.getCode()); + + return new org.eclipse.californium.core.observe.Observation(coapRequest, null); + } + private void givenASimpleRegistration(Long lifetime) { Registration.Builder builder = new Registration.Builder(registrationId, ep, Identity.unsecure(address, port)); diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilderTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilderTest.java index e34e414a9d..c2aee3500a 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilderTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilderTest.java @@ -17,14 +17,20 @@ import static org.junit.Assert.*; +import java.util.Arrays; +import java.util.List; import java.util.Map; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.coap.Token; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.SingleObservation; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.server.californium.DummyDecoder; import org.eclipse.leshan.server.californium.observation.ObserveUtil; @@ -62,4 +68,35 @@ public void visit_observe_request() { assertEquals(examplePath, observation.getPath().toString()); } + @Test + public void visit_observe_composite_request() { + // given + List examplePaths = Arrays.asList(new LwM2mPath("/1/2/3"), new LwM2mPath("/4/5/6")); + + ObserveCompositeRequest observeRequest = new ObserveCompositeRequest(null, null, examplePaths); + + Map userContext = ObserveUtil.createCoapObserveCompositeRequestContext(null, null, + observeRequest); + + Request coapRequest = new Request(null); + coapRequest.setToken(Token.EMPTY); + coapRequest.setUserContext(userContext); + + Response coapResponse = new Response(CoAP.ResponseCode.CONTENT); + coapResponse.getOptions().setObserve(1); + + LwM2mResponseBuilder responseBuilder = new LwM2mResponseBuilder<>(coapRequest, + coapResponse, null, null, new DummyDecoder()); + // when + responseBuilder.visit(observeRequest); + + // then + ObserveCompositeResponse response = responseBuilder.getResponse(); + assertNotNull(response); + assertNotNull(response.getObservation()); + + CompositeObservation observation = response.getObservation(); + assertEquals(examplePaths, observation.getPaths()); + } + } diff --git a/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java b/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java index b5577dcda5..bebc2f49dd 100644 --- a/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java +++ b/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java @@ -16,6 +16,7 @@ * EndpointContext * Achim Kraus (Bosch Software Innovations GmbH) - update to modified * ObservationStore API + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.server.redis; @@ -35,12 +36,14 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Token; import org.eclipse.californium.core.observe.ObservationStoreException; import org.eclipse.californium.elements.EndpointContext; import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Stoppable; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.Identity; @@ -522,6 +525,9 @@ private boolean areTheSamePaths(Observation observation, Observation obs) { if (observation instanceof SingleObservation && obs instanceof SingleObservation) { return ((SingleObservation) observation).getPath().equals(((SingleObservation) obs).getPath()); } + if (observation instanceof CompositeObservation && obs instanceof CompositeObservation) { + return ((CompositeObservation) observation).getPaths().equals(((CompositeObservation) obs).getPaths()); + } return false; } @@ -758,7 +764,13 @@ private Observation build(org.eclipse.californium.core.observe.Observation cfObs if (cfObs == null) return null; - return ObserveUtil.createLwM2mObservation(cfObs.getRequest()); + if (cfObs.getRequest().getCode() == CoAP.Code.GET) { + return ObserveUtil.createLwM2mObservation(cfObs.getRequest()); + } else if (cfObs.getRequest().getCode() == CoAP.Code.FETCH) { + return ObserveUtil.createLwM2mCompositeObservation(cfObs.getRequest()); + } else { + throw new IllegalStateException("Observation request can be GET or FETCH only"); + } } /* *************** Expiration handling **************** */ From 5767475c4e77ab2aaf4d1b9d26010ba147453777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wadowski?= Date: Thu, 29 Jul 2021 13:56:15 +0200 Subject: [PATCH 3/8] Add client side Observe Composite feature --- .../ObserveCompositeRelationFilter.java | 95 +++++++++++++++++++ .../client/californium/RootResource.java | 55 ++++++++--- .../californium/object/ObjectResource.java | 5 +- .../client/resource/LwM2mObjectTree.java | 2 +- .../client/resource/LwM2mRootEnabler.java | 10 ++ .../leshan/client/resource/RootEnabler.java | 55 +++++++++++ 6 files changed, 207 insertions(+), 15 deletions(-) create mode 100644 leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/ObserveCompositeRelationFilter.java diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/ObserveCompositeRelationFilter.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/ObserveCompositeRelationFilter.java new file mode 100644 index 0000000000..dc1559cf99 --- /dev/null +++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/ObserveCompositeRelationFilter.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2021 Orange. + * + * 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: + * Michał Wadowski (Orange) - Add Observe-Composite feature. + *******************************************************************************/ +package org.eclipse.leshan.client.californium; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.observe.ObserveRelation; +import org.eclipse.californium.core.observe.ObserveRelationFilter; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.codec.CodecException; +import org.eclipse.leshan.core.node.codec.LwM2mDecoder; +import org.eclipse.leshan.core.request.ContentFormat; + +/** + * An {@link ObserveRelationFilter} which select {@link ObserveRelation} based on one of resource URIs. + */ +class ObserveCompositeRelationFilter implements ObserveRelationFilter { + + private final LwM2mDecoder decoder; + private final List paths; + + /** + * Instantiates {@link ObserveCompositeRelationFilter} basing on resource URIs. + * + * @param decoder {@link LwM2mDecoder} which will decode data in supported content format. + * @param paths the list of {@link LwM2mPath} + */ + public ObserveCompositeRelationFilter(LwM2mDecoder decoder, LwM2mPath... paths) { + this.decoder = decoder; + this.paths = Arrays.asList(paths); + } + + @Override + public boolean accept(ObserveRelation relation) { + Request request = getRequest(relation); + + if (!isValidObserveCompositeRequest(request)) { + return false; + } + + List observationPaths = getObserveRequestPaths(request); + + if (observationPaths != null) { + for (LwM2mPath observePath : observationPaths) { + for (LwM2mPath path : paths) { + if (path.startWith(observePath)) { + return true; + } + } + } + } + + return false; + } + + private boolean isValidObserveCompositeRequest(Request request) { + if (!request.getCode().equals(CoAP.Code.FETCH)) { + return false; + } + if (!request.getOptions().hasContentFormat()) { + return false; + } + return true; + } + + private Request getRequest(ObserveRelation relation) { + return relation.getExchange().getRequest(); + } + + private List getObserveRequestPaths(Request request) { + ContentFormat contentFormat = ContentFormat.fromCode(request.getOptions().getContentFormat()); + + try { + return decoder.decodePaths(request.getPayload(), contentFormat); + } catch (CodecException e) { + return null; + } + } +} diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/RootResource.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/RootResource.java index 27a0a7b481..3a596bd57f 100644 --- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/RootResource.java +++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/RootResource.java @@ -13,6 +13,7 @@ * Contributors: * Sierra Wireless - initial API and implementation * Achim Kraus (Bosch Software Innovations GmbH) - use ServerIdentity + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.client.californium; @@ -30,6 +31,7 @@ import org.eclipse.leshan.client.bootstrap.BootstrapHandler; import org.eclipse.leshan.client.engine.RegistrationEngine; import org.eclipse.leshan.client.resource.LwM2mRootEnabler; +import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter; import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.Link; import org.eclipse.leshan.core.node.LwM2mNode; @@ -39,10 +41,12 @@ import org.eclipse.leshan.core.request.BootstrapDeleteRequest; import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ReadCompositeRequest; import org.eclipse.leshan.core.request.WriteCompositeRequest; import org.eclipse.leshan.core.response.BootstrapDeleteResponse; import org.eclipse.leshan.core.response.BootstrapDiscoverResponse; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ReadCompositeResponse; import org.eclipse.leshan.core.response.WriteCompositeResponse; import org.eclipse.leshan.core.util.StringUtils; @@ -64,10 +68,13 @@ public RootResource(RegistrationEngine registrationEngine, CaliforniumEndpointsM super("", registrationEngine, endpointsManager); this.bootstrapHandler = bootstrapHandler; setVisible(false); + setObservable(true); this.coapServer = coapServer; this.rootEnabler = rootEnabler; this.encoder = encoder; this.decoder = decoder; + + addListeners(); } @Override @@ -97,7 +104,6 @@ public void handleFETCH(CoapExchange exchange) { if (identity == null) return; - // Manage Read Composite request Request coapRequest = exchange.advanced().getRequest(); // Handle content format for the response @@ -119,18 +125,36 @@ public void handleFETCH(CoapExchange exchange) { ContentFormat requestContentFormat = ContentFormat.fromCode(exchange.getRequestOptions().getContentFormat()); List paths = decoder.decodePaths(coapRequest.getPayload(), requestContentFormat); - ReadCompositeResponse response = rootEnabler.read(identity, - new ReadCompositeRequest(paths, requestContentFormat, responseContentFormat, coapRequest)); - if (response.getCode().isError()) { - exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage()); + if (exchange.getRequestOptions().hasObserve()) { + // Manage Observe Composite request + ObserveCompositeRequest observeRequest = new ObserveCompositeRequest(requestContentFormat, + responseContentFormat, paths, coapRequest); + ObserveCompositeResponse response = rootEnabler.observe(identity, observeRequest); + + if (response.getCode().isError()) { + exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage()); + return; + } else { + exchange.respond(toCoapResponseCode(response.getCode()), + encoder.encodeNodes(response.getContent(), responseContentFormat, rootEnabler.getModel()), + responseContentFormat.getCode()); + return; + } } else { - // TODO we could maybe face some race condition if an objectEnabler is removed from LwM2mObjectTree between - // rootEnabler.read() and rootEnabler.getModel() - exchange.respond(toCoapResponseCode(response.getCode()), - encoder.encodeNodes(response.getContent(), responseContentFormat, rootEnabler.getModel()), - responseContentFormat.getCode()); + // Manage Read Composite request + ReadCompositeResponse response = rootEnabler.read(identity, + new ReadCompositeRequest(paths, requestContentFormat, responseContentFormat, coapRequest)); + if (response.getCode().isError()) { + exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage()); + } else { + // TODO we could maybe face some race condition if an objectEnabler is removed from LwM2mObjectTree + // between rootEnabler.read() and rootEnabler.getModel() + exchange.respond(toCoapResponseCode(response.getCode()), + encoder.encodeNodes(response.getContent(), responseContentFormat, rootEnabler.getModel()), + responseContentFormat.getCode()); + } + return; } - return; } @Override @@ -180,4 +204,13 @@ public void handleDELETE(CoapExchange exchange) { exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage()); } + private void addListeners() { + rootEnabler.addListener(new ObjectsListenerAdapter() { + + @Override + public void resourceChanged(LwM2mPath... paths) { + changed(new ObserveCompositeRelationFilter(decoder, paths)); + } + }); + } } 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 96be9d3035..dbde2346ea 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 @@ -16,6 +16,7 @@ * Achim Kraus (Bosch Software Innovations GmbH) - use ServerIdentity * Achim Kraus (Bosch Software Innovations GmbH) - implement POST "/oid/iid" * as UPDATE instance + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.client.californium.object; @@ -153,9 +154,7 @@ public void handleGET(CoapExchange exchange) { exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage()); return; } - } - - else { + } else { if (identity.isLwm2mBootstrapServer()) { // Manage Bootstrap Read Request BootstrapReadRequest readRequest = new BootstrapReadRequest(requestedContentFormat, URI, diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectTree.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectTree.java index 95da9c0720..8900fd72c5 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectTree.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mObjectTree.java @@ -93,7 +93,7 @@ public void addListener(ObjectsListener listener) { listeners.add(listener); } - public void removedListener(ObjectsListener listener) { + public void removeListener(ObjectsListener listener) { listeners.remove(listener); } diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mRootEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mRootEnabler.java index 6b33f00fc4..251ef5188a 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mRootEnabler.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mRootEnabler.java @@ -12,13 +12,17 @@ * * Contributors: * Sierra Wireless - initial API and implementation + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.client.resource; +import org.eclipse.leshan.client.resource.listener.ObjectsListener; import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ReadCompositeRequest; import org.eclipse.leshan.core.request.WriteCompositeRequest; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ReadCompositeResponse; import org.eclipse.leshan.core.response.WriteCompositeResponse; @@ -32,4 +36,10 @@ public interface LwM2mRootEnabler { WriteCompositeResponse write(ServerIdentity identity, WriteCompositeRequest request); LwM2mModel getModel(); + + ObserveCompositeResponse observe(ServerIdentity identity, ObserveCompositeRequest request); + + void addListener(ObjectsListener listener); + + void removeListener(ObjectsListener listener); } diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/RootEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/RootEnabler.java index 649db34e89..2f3161e391 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/RootEnabler.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/RootEnabler.java @@ -12,6 +12,7 @@ * * Contributors: * Sierra Wireless - initial API and implementation + * Michał Wadowski (Orange) - Add Observe-Composite feature. *******************************************************************************/ package org.eclipse.leshan.client.resource; @@ -20,6 +21,7 @@ import java.util.Map; import java.util.Map.Entry; +import org.eclipse.leshan.client.resource.listener.ObjectsListener; import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.model.ObjectModel; @@ -31,11 +33,14 @@ import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mResourceInstance; import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; +import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.core.request.ReadCompositeRequest; import org.eclipse.leshan.core.request.ReadRequest; import org.eclipse.leshan.core.request.WriteCompositeRequest; import org.eclipse.leshan.core.request.WriteRequest; import org.eclipse.leshan.core.request.WriteRequest.Mode; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ReadCompositeResponse; import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.core.response.WriteCompositeResponse; @@ -56,6 +61,16 @@ public RootEnabler(final LwM2mObjectTree tree) { this.tree = tree; } + @Override + public void addListener(ObjectsListener listener) { + tree.addListener(listener); + } + + @Override + public void removeListener(ObjectsListener listener) { + tree.removeListener(listener); + } + @Override public ReadCompositeResponse read(ServerIdentity identity, ReadCompositeRequest request) { List paths = request.getPaths(); @@ -193,6 +208,46 @@ public WriteCompositeResponse write(ServerIdentity identity, WriteCompositeReque } + @Override + public synchronized ObserveCompositeResponse observe(ServerIdentity identity, ObserveCompositeRequest request) { + List paths = request.getPaths(); + + // Read Nodes + Map content = new HashMap<>(); + boolean isEmpty = true; // true if don't succeed to read any of requested path + for (LwM2mPath path : paths) { + // Get corresponding object enabler + Integer objectId = path.getObjectId(); + LwM2mObjectEnabler objectEnabler = tree.getObjectEnabler(objectId); + + LwM2mNode node = null; + if (objectEnabler != null) { + ReadResponse response = objectEnabler.observe(identity, + new ObserveRequest(request.getResponseContentFormat(), path, request.getCoapRequest()) + ); + if (response.isSuccess()) { + node = response.getContent(); + isEmpty = false; + } else { + LOG.debug("Server {} try to read node {} in a Observe-Composite Request {} but it failed for {} " + + "{}", identity, path, paths, response.getCode(), response.getErrorMessage() + ); + } + } else { + LOG.debug("Server {} try to read node {} in a Observe-Composite Request {} but it failed because " + + "Object {} is not supported", identity, path, paths, objectId + ); + } + + content.put(path, node); + } + if (isEmpty) { + return ObserveCompositeResponse.notFound(); + } else { + return ObserveCompositeResponse.success(content); + } + } + @Override public LwM2mModel getModel() { return tree.getModel(); From 84293b26e8a6ff796a25434bc0cdcd0f82da1687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wadowski?= Date: Thu, 29 Jul 2021 14:03:31 +0200 Subject: [PATCH 4/8] Add integration tests about Observe-Composite --- .../tests/observe/ObserveCompositeTest.java | 345 ++++++++++++++++++ .../redis/RedisRegistrationStoreTest.java | 37 ++ 2 files changed, 382 insertions(+) create mode 100644 leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTest.java diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTest.java new file mode 100644 index 0000000000..20b9dd6a7b --- /dev/null +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTest.java @@ -0,0 +1,345 @@ +/******************************************************************************* + * Copyright (c) 2021 Orange. + * + * 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: + * Michał Wadowski (Orange) - Add Observe-Composite feature. + *******************************************************************************/ +package org.eclipse.leshan.integration.tests.observe; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.californium.core.coap.Response; +import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.observation.CompositeObservation; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; +import org.eclipse.leshan.core.request.ReadRequest; +import org.eclipse.leshan.core.request.WriteCompositeRequest; +import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.response.LwM2mResponse; +import org.eclipse.leshan.core.response.ObserveCompositeResponse; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.core.response.WriteCompositeResponse; +import org.eclipse.leshan.integration.tests.util.IntegrationTestHelper; +import org.eclipse.leshan.server.registration.Registration; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ObserveCompositeTest { + + protected IntegrationTestHelper helper = new IntegrationTestHelper(); + private Registration currentRegistration; + private TestObservationListener listener; + + @Before + public void start() { + helper.initialize(); + helper.createServer(); + helper.server.start(); + helper.createClient(); + helper.client.start(); + helper.waitForRegistrationAtServerSide(1); + + currentRegistration = helper.getCurrentRegistration(); + listener = new TestObservationListener(); + helper.server.getObservationService().addListener(listener); + } + + @After + public void stop() { + helper.client.destroy(false); + helper.server.destroy(); + helper.dispose(); + } + + @Test + public void can_composite_observe_on_single_resource() throws InterruptedException { + // Send ObserveCompositeRequest + ObserveCompositeResponse observeResponse = helper.server.send(currentRegistration, + new ObserveCompositeRequest(ContentFormat.SENML_JSON, ContentFormat.SENML_JSON, "/3/0/15")); + + // Assert that ObserveCompositeResponse is valid + assertEquals(ResponseCode.CONTENT, observeResponse.getCode()); + assertNotNull(observeResponse.getCoapResponse()); + assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); + + CompositeObservation observation = observeResponse.getObservation(); + + // Assert that CompositeObservation contains expected paths + assertNotNull(observation); + assertEquals(1, observation.getPaths().size()); + assertEquals("/3/0/15", observation.getPaths().get(0).toString()); + + // Assert that there is one valid observation + assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); + Set observations = helper.server.getObservationService() + .getObservations(helper.getCurrentRegistration()); + assertEquals("We should have only one observation", 1, observations.size()); + assertTrue("New observation is not there", observations.contains(observation)); + + // Write single example value + LwM2mResponse writeResponse = helper.server.send(helper.getCurrentRegistration(), + new WriteRequest(3, 0, 15, "Europe/Paris")); + listener.waitForNotification(2000); + assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); + + // Assert that response contains expected paths + assertTrue(listener.receivedNotify().get()); + Map content = listener.getObserveCompositeResponse().getContent(); + assertEquals(1, content.size()); + assertTrue(content.containsKey(new LwM2mPath("/3/0/15"))); + + // Assert that listener response contains expected values + assertEquals(LwM2mSingleResource.newStringResource(15, "Europe/Paris"), content.get(new LwM2mPath("/3/0/15"))); + + // Assert that listener has Response + assertNotNull(listener.getObserveCompositeResponse().getCoapResponse()); + assertThat(listener.getObserveCompositeResponse().getCoapResponse(), is(instanceOf(Response.class))); + } + + @Test + public void should_not_get_response_if_modified_other_resource_than_observed() throws InterruptedException { + // Send ObserveCompositeRequest + ObserveCompositeResponse observeResponse = helper.server.send(currentRegistration, + new ObserveCompositeRequest(ContentFormat.SENML_JSON, ContentFormat.SENML_JSON, "/3/0/14")); + + // Assert that ObserveCompositeResponse is valid + assertEquals(ResponseCode.CONTENT, observeResponse.getCode()); + assertNotNull(observeResponse.getCoapResponse()); + assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); + + CompositeObservation observation = observeResponse.getObservation(); + + // Assert that CompositeObservation contains expected paths + assertNotNull(observation); + assertEquals(1, observation.getPaths().size()); + assertEquals("/3/0/14", observation.getPaths().get(0).toString()); + + // Assert that there is one valid observation + assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); + Set observations = helper.server.getObservationService() + .getObservations(helper.getCurrentRegistration()); + assertEquals("We should have only one observation", 1, observations.size()); + assertTrue("New observation is not there", observations.contains(observation)); + + // Write single example value + LwM2mResponse writeResponse = helper.server.send(helper.getCurrentRegistration(), + new WriteRequest(3, 0, 15, "Europe/Paris")); + listener.waitForNotification(2000); + assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); + + // Assert that listener has no response + assertFalse(listener.receivedNotify().get()); + } + + @Test + public void can_composite_observe_on_multiple_resources() throws InterruptedException { + // Send ObserveCompositeRequest + ObserveCompositeResponse observeResponse = helper.server.send(currentRegistration, + new ObserveCompositeRequest(ContentFormat.SENML_JSON, ContentFormat.SENML_JSON, "/3/0/15", "/3/0/14")); + + // Assert that ObserveCompositeResponse is valid + assertEquals(ResponseCode.CONTENT, observeResponse.getCode()); + assertNotNull(observeResponse.getCoapResponse()); + assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); + + CompositeObservation observation = observeResponse.getObservation(); + + // Assert that CompositeObservation contains expected paths + assertNotNull(observation); + assertEquals(2, observation.getPaths().size()); + assertEquals("/3/0/15", observation.getPaths().get(0).toString()); + assertEquals("/3/0/14", observation.getPaths().get(1).toString()); + + // Assert that there is one valid observation + assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); + Set observations = helper.server.getObservationService() + .getObservations(helper.getCurrentRegistration()); + assertEquals("We should have only one observation", 1, observations.size()); + assertTrue("New observation is not there", observations.contains(observation)); + + // Write single example value + LwM2mResponse writeResponse = helper.server.send(helper.getCurrentRegistration(), + new WriteRequest(3, 0, 15, "Europe/Paris")); + listener.waitForNotification(2000); + assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); + + // Assert that response contains exactly the same paths + assertTrue(listener.receivedNotify().get()); + Map content = listener.getObserveCompositeResponse().getContent(); + assertEquals(2, content.size()); + assertTrue(content.containsKey(new LwM2mPath("/3/0/15"))); + assertTrue(content.containsKey(new LwM2mPath("/3/0/14"))); + + // Assert that listener response contains expected values + assertEquals(LwM2mSingleResource.newStringResource(new LwM2mPath("/3/0/15").getResourceId(), "Europe/Paris"), + content.get(new LwM2mPath("/3/0/15"))); + assertEquals(LwM2mSingleResource.newStringResource(new LwM2mPath("/3/0/14").getResourceId(), "+02"), + content.get(new LwM2mPath("/3/0/14"))); + + // Assert that listener has Response + assertNotNull(listener.getObserveCompositeResponse().getCoapResponse()); + assertThat(listener.getObserveCompositeResponse().getCoapResponse(), is(instanceOf(Response.class))); + } + + @Test + public void can_composite_observe_on_multiple_resources_with_write_composite() throws InterruptedException { + // Send ObserveCompositeRequest + ObserveCompositeResponse observeResponse = helper.server.send(currentRegistration, + new ObserveCompositeRequest(ContentFormat.SENML_JSON, ContentFormat.SENML_JSON, "/3/0/15", "/3/0/14")); + + // Assert that ObserveCompositeResponse is valid + assertEquals(ResponseCode.CONTENT, observeResponse.getCode()); + assertNotNull(observeResponse.getCoapResponse()); + assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); + + CompositeObservation observation = observeResponse.getObservation(); + + // Assert that CompositeObservation contains expected paths + assertNotNull(observation); + assertEquals(2, observation.getPaths().size()); + assertEquals("/3/0/15", observation.getPaths().get(0).toString()); + assertEquals("/3/0/14", observation.getPaths().get(1).toString()); + + // Assert that there is one valid observation + assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); + Set observations = helper.server.getObservationService() + .getObservations(helper.getCurrentRegistration()); + assertEquals("We should have only one observation", 1, observations.size()); + assertTrue("New observation is not there", observations.contains(observation)); + + // Write example composite values + Map nodes = new HashMap<>(); + nodes.put("/3/0/15", "Europe/Paris"); + nodes.put("/3/0/14", "+11"); + WriteCompositeResponse writeResponse = helper.server.send(helper.getCurrentRegistration(), + new WriteCompositeRequest(ContentFormat.SENML_JSON, nodes)); + listener.waitForNotification(2000); + assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); + + // Assert that response contains expected paths + assertTrue(listener.receivedNotify().get()); + Map content = listener.getObserveCompositeResponse().getContent(); + assertEquals(2, content.size()); + assertTrue(content.containsKey(new LwM2mPath("/3/0/15"))); + assertTrue(content.containsKey(new LwM2mPath("/3/0/14"))); + + // Assert that listener response contains expected values + assertEquals(LwM2mSingleResource.newStringResource(new LwM2mPath("/3/0/15").getResourceId(), "Europe/Paris"), + content.get(new LwM2mPath("/3/0/15"))); + assertEquals(LwM2mSingleResource.newStringResource(new LwM2mPath("/3/0/14").getResourceId(), "+11"), + content.get(new LwM2mPath("/3/0/14"))); + + // Assert that listener has Response + assertNotNull(listener.getObserveCompositeResponse().getCoapResponse()); + assertThat(listener.getObserveCompositeResponse().getCoapResponse(), is(instanceOf(Response.class))); + } + + @Test + public void can_observe_instance() throws InterruptedException { + // Send ObserveCompositeRequest + ObserveCompositeResponse observeResponse = helper.server.send(currentRegistration, + new ObserveCompositeRequest(ContentFormat.SENML_JSON, ContentFormat.SENML_JSON, "/3/0")); + + // Assert that ObserveCompositeResponse is valid + assertEquals(ResponseCode.CONTENT, observeResponse.getCode()); + assertNotNull(observeResponse.getCoapResponse()); + assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); + + CompositeObservation observation = observeResponse.getObservation(); + + // Assert that CompositeObservation contains expected paths + assertNotNull(observation); + assertEquals(1, observation.getPaths().size()); + assertEquals("/3/0", observation.getPaths().get(0).toString()); + + // Assert that there is one valid observation + assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); + Set observations = helper.server.getObservationService() + .getObservations(helper.getCurrentRegistration()); + assertEquals("We should have only one observation", 1, observations.size()); + assertTrue("New observation is not there", observations.contains(observation)); + + // Write single example value + LwM2mResponse writeResponse = helper.server.send(helper.getCurrentRegistration(), + new WriteRequest(3, 0, 15, "Europe/Paris")); + listener.waitForNotification(2000); + assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); + + // Assert that response contains expected paths + assertTrue(listener.receivedNotify().get()); + Map content = listener.getObserveCompositeResponse().getContent(); + assertEquals(1, content.size()); + assertTrue(content.containsKey(new LwM2mPath("/3/0"))); + + // Assert that listener response equals to ReadResponse + ReadResponse readResp = helper.server.send(helper.getCurrentRegistration(), + new ReadRequest(ContentFormat.SENML_JSON, "/3/0")); + assertEquals(readResp.getContent(), content.get(new LwM2mPath("/3/0"))); + } + + @Test + public void can_observe_object() throws InterruptedException { + // Send ObserveCompositeRequest + ObserveCompositeResponse observeResponse = helper.server.send(currentRegistration, + new ObserveCompositeRequest(ContentFormat.SENML_JSON, ContentFormat.SENML_JSON, "/3")); + + // Assert that ObserveCompositeResponse is valid + assertEquals(ResponseCode.CONTENT, observeResponse.getCode()); + assertNotNull(observeResponse.getCoapResponse()); + assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class))); + + CompositeObservation observation = observeResponse.getObservation(); + + // Assert that CompositeObservation contains expected paths + assertNotNull(observation); + assertEquals(1, observation.getPaths().size()); + assertEquals("/3", observation.getPaths().get(0).toString()); + + // Assert that there is one valid observation + assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); + Set observations = helper.server.getObservationService() + .getObservations(helper.getCurrentRegistration()); + assertEquals("We should have only one observation", 1, observations.size()); + assertTrue("New observation is not there", observations.contains(observation)); + + // Write single example value + LwM2mResponse writeResponse = helper.server.send(helper.getCurrentRegistration(), + new WriteRequest(3, 0, 15, "Europe/Paris")); + listener.waitForNotification(2000); + assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); + + // Assert that response contains expected paths + assertTrue(listener.receivedNotify().get()); + Map content = listener.getObserveCompositeResponse().getContent(); + assertEquals(1, content.size()); + assertTrue(content.containsKey(new LwM2mPath("/3"))); + + // Assert that listener response equals to ReadResponse + ReadResponse readResp = helper.server.send(helper.getCurrentRegistration(), + new ReadRequest(ContentFormat.SENML_JSON, "/3")); + assertEquals(readResp.getContent(), content.get(new LwM2mPath("/3"))); + } + +} diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/server/redis/RedisRegistrationStoreTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/server/redis/RedisRegistrationStoreTest.java index 6c9625021a..e46c3fed85 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/server/redis/RedisRegistrationStoreTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/server/redis/RedisRegistrationStoreTest.java @@ -21,7 +21,9 @@ import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.EnumSet; +import java.util.List; import java.util.Map; import org.eclipse.californium.core.coap.CoAP; @@ -29,11 +31,14 @@ import org.eclipse.californium.core.coap.Token; import org.eclipse.californium.elements.AddressEndpointContext; import org.eclipse.leshan.core.Link; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.BindingMode; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.integration.tests.util.RedisIntegrationTestHelper; import org.eclipse.leshan.server.californium.observation.ObserveUtil; @@ -95,6 +100,29 @@ public void get_observation_from_request() { assertEquals(examplePath, observation.getPath().toString()); } + @Test + public void get_composite_observation_from_request() { + // given + List examplePaths = Arrays.asList(new LwM2mPath("/1/2/3"), new LwM2mPath("/4/5/6")); + Token exampleToken = Token.EMPTY; + + givenASimpleRegistration(lifetime); + store.addRegistration(registration); + + org.eclipse.californium.core.observe.Observation observationToStore = prepareCoapObservationOnComposite( + examplePaths); + + // when + store.put(exampleToken, observationToStore); + + // then + Observation leshanObservation = store.getObservation(registrationId, exampleToken.getBytes()); + assertNotNull(leshanObservation); + assertTrue(leshanObservation instanceof CompositeObservation); + CompositeObservation observation = (CompositeObservation) leshanObservation; + assertEquals(examplePaths, observation.getPaths()); + } + private void givenASimpleRegistration(Long lifetime) { Registration.Builder builder = new Registration.Builder(registrationId, ep, Identity.unsecure(address, port)); @@ -111,6 +139,15 @@ private org.eclipse.californium.core.observe.Observation prepareCoapObservationO return prepareCoapObservation(new Request(CoAP.Code.GET), userContext); } + private org.eclipse.californium.core.observe.Observation prepareCoapObservationOnComposite(List paths) { + ObserveCompositeRequest observeRequest = new ObserveCompositeRequest(null, null, paths); + + Map userContext = ObserveUtil.createCoapObserveCompositeRequestContext(ep, registrationId, + observeRequest); + + return prepareCoapObservation(new Request(CoAP.Code.FETCH), userContext); + } + private org.eclipse.californium.core.observe.Observation prepareCoapObservation(Request coapRequest, Map userContext) { coapRequest.setUserContext(userContext); From 578c2845a8f8139772363e36b24d473ea1a4beba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wadowski?= Date: Fri, 30 Jul 2021 09:32:45 +0200 Subject: [PATCH 5/8] Refactor Observation request & response content format --- .../observation/CompositeObservation.java | 47 ++++++++++++++++--- .../leshan/core/observation/Observation.java | 24 ++-------- .../core/observation/SingleObservation.java | 21 ++++++++- .../request/CancelObservationRequest.java | 6 ++- .../request/CompositeDownlinkRequest.java | 6 +-- .../californium/observation/ObserveUtil.java | 14 ++++-- .../request/CoapRequestBuilder.java | 4 +- .../observation/ObserveUtilTest.java | 7 ++- 8 files changed, 89 insertions(+), 40 deletions(-) diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/CompositeObservation.java b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/CompositeObservation.java index 3b33c700e1..d99ddf2b24 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/CompositeObservation.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/CompositeObservation.java @@ -12,12 +12,14 @@ * * Contributors: * Michał Wadowski (Orange) - Add Observe-Composite feature. + * Michał Wadowski (Orange) - Add Cancel Composite-Observation feature. *******************************************************************************/ package org.eclipse.leshan.core.observation; import java.util.List; import java.util.Map; +import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.util.Hex; @@ -28,6 +30,8 @@ public class CompositeObservation extends Observation { private final List paths; + private final ContentFormat requestContentFormat; + private final ContentFormat responseContentFormat; /** * Instantiates an {@link CompositeObservation} for the given node paths. @@ -35,15 +39,32 @@ public class CompositeObservation extends Observation { * @param id token identifier of the observation * @param registrationId client's unique registration identifier. * @param paths resources paths for which the composite-observation is set. - * @param contentFormat contentFormat used to read the resource (could be null). + * @param requestContentFormat The {@link ContentFormat} used to encode the list of {@link LwM2mPath} + * @param responseContentFormat The {@link ContentFormat} requested to encode the {@link LwM2mNode} of the response. * @param context additional information relative to this observation. */ - public CompositeObservation(byte[] id, String registrationId, List paths, ContentFormat contentFormat, - Map context) { - super(id, registrationId, contentFormat, context); + public CompositeObservation(byte[] id, String registrationId, List paths, + ContentFormat requestContentFormat, ContentFormat responseContentFormat, Map context) { + super(id, registrationId, context); + this.requestContentFormat = requestContentFormat; + this.responseContentFormat = responseContentFormat; this.paths = paths; } + /** + * @return the {@link ContentFormat} used to encode the list of {@link LwM2mPath} + */ + public ContentFormat getRequestContentFormat() { + return requestContentFormat; + } + + /** + * @return the {@link ContentFormat} requested to encode the {@link LwM2mNode} of the response. + */ + public ContentFormat getResponseContentFormat() { + return responseContentFormat; + } + /** * Gets the observed resources paths. * @@ -55,8 +76,9 @@ public List getPaths() { @Override public String toString() { - return String.format("CompositeObservation [paths=%s, id=%s, contentFormat=%s, registrationId=%s, context=%s]", - paths, Hex.encodeHexString(id), contentFormat, registrationId, context); + return String.format( + "CompositeObservation [paths=%s, id=%s, requestContentFormat=%s, responseContentFormat=%s, registrationId=%s, context=%s]", + paths, Hex.encodeHexString(id), requestContentFormat, responseContentFormat, registrationId, context); } @Override @@ -64,6 +86,8 @@ public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + ((paths == null) ? 0 : paths.hashCode()); + result = prime * result + ((requestContentFormat == null) ? 0 : requestContentFormat.hashCode()); + result = prime * result + ((responseContentFormat == null) ? 0 : responseContentFormat.hashCode()); return result; } @@ -81,6 +105,17 @@ public boolean equals(Object obj) { return false; } else if (!paths.equals(other.paths)) return false; + if (requestContentFormat == null) { + if (other.requestContentFormat != null) + return false; + } else if (!requestContentFormat.equals(other.requestContentFormat)) + return false; + if (responseContentFormat == null) { + if (other.responseContentFormat != null) + return false; + } else if (!responseContentFormat.equals(other.responseContentFormat)) + return false; return true; } + } diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/Observation.java b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/Observation.java index d588b74ba0..24f92786bc 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/Observation.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/Observation.java @@ -13,6 +13,7 @@ * Contributors: * Sierra Wireless - initial API and implementation * Michał Wadowski (Orange) - Add Observe-Composite feature. + * Michał Wadowski (Orange) - Add Cancel Composite-Observation feature. *******************************************************************************/ package org.eclipse.leshan.core.observation; @@ -21,15 +22,12 @@ import java.util.HashMap; import java.util.Map; -import org.eclipse.leshan.core.request.ContentFormat; - /** * An abstract class for observation of a resource provided by a LWM2M Client. */ public abstract class Observation { protected final byte[] id; - protected final ContentFormat contentFormat; protected final String registrationId; protected final Map context; @@ -38,12 +36,10 @@ public abstract class Observation { * * @param id token identifier of the observation * @param registrationId client's unique registration identifier. - * @param contentFormat contentFormat used to read the resource (could be null). * @param context additional information relative to this observation. */ - public Observation(byte[] id, String registrationId, ContentFormat contentFormat, Map context) { + public Observation(byte[] id, String registrationId, Map context) { this.id = id; - this.contentFormat = contentFormat; this.registrationId = registrationId; if (context != null) this.context = Collections.unmodifiableMap(new HashMap<>(context)); @@ -68,15 +64,6 @@ public String getRegistrationId() { return registrationId; } - /** - * Gets the requested contentFormat (could be null). - * - * @return the resource path - */ - public ContentFormat getContentFormat() { - return contentFormat; - } - /** * @return the contextual information relative to this observation. */ @@ -88,7 +75,6 @@ public Map getContext() { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((contentFormat == null) ? 0 : contentFormat.hashCode()); result = prime * result + ((context == null) ? 0 : context.hashCode()); result = prime * result + Arrays.hashCode(id); result = prime * result + ((registrationId == null) ? 0 : registrationId.hashCode()); @@ -104,11 +90,6 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) return false; Observation other = (Observation) obj; - if (contentFormat == null) { - if (other.contentFormat != null) - return false; - } else if (!contentFormat.equals(other.contentFormat)) - return false; if (context == null) { if (other.context != null) return false; @@ -123,4 +104,5 @@ public boolean equals(Object obj) { return false; return true; } + } diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/SingleObservation.java b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/SingleObservation.java index 58c957c921..a86c3a6886 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/SingleObservation.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/SingleObservation.java @@ -13,6 +13,7 @@ * Contributors: * Sierra Wireless - initial API and implementation * Michał Wadowski (Orange) - Add Observe-Composite feature. + * Michał Wadowski (Orange) - Add Cancel Composite-Observation feature. *******************************************************************************/ package org.eclipse.leshan.core.observation; @@ -29,6 +30,8 @@ public class SingleObservation extends Observation { private final LwM2mPath path; + protected final ContentFormat contentFormat; + /** * Instantiates an {@link SingleObservation} for the given node path. * @@ -40,8 +43,9 @@ public class SingleObservation extends Observation { */ public SingleObservation(byte[] id, String registrationId, LwM2mPath path, ContentFormat contentFormat, Map context) { - super(id, registrationId, contentFormat, context); + super(id, registrationId, context); this.path = path; + this.contentFormat = contentFormat; } /** @@ -53,6 +57,15 @@ public LwM2mPath getPath() { return path; } + /** + * Gets the requested contentFormat (could be null). + * + * @return the resource path + */ + public ContentFormat getContentFormat() { + return contentFormat; + } + @Override public String toString() { return String.format("SingleObservation [path=%s, id=%s, contentFormat=%s, registrationId=%s, context=%s]", @@ -63,6 +76,7 @@ public String toString() { public int hashCode() { final int prime = 31; int result = super.hashCode(); + result = prime * result + ((contentFormat == null) ? 0 : contentFormat.hashCode()); result = prime * result + ((path == null) ? 0 : path.hashCode()); return result; } @@ -76,6 +90,11 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) return false; SingleObservation other = (SingleObservation) obj; + if (contentFormat == null) { + if (other.contentFormat != null) + return false; + } else if (!contentFormat.equals(other.contentFormat)) + return false; if (path == null) { if (other.path != null) return false; diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelObservationRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelObservationRequest.java index efe7c5a061..7ee475b30c 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelObservationRequest.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelObservationRequest.java @@ -30,7 +30,7 @@ */ public class CancelObservationRequest extends AbstractSimpleDownlinkRequest { - private final Observation observation; + private final SingleObservation observation; /** * @param observation the observation to cancel actively @@ -46,6 +46,10 @@ public Observation getObservation() { return observation; } + public ContentFormat getContentFormat() { + return observation.getContentFormat(); + } + @Override public void accept(DownlinkRequestVisitor visitor) { visitor.visit(this); diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CompositeDownlinkRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CompositeDownlinkRequest.java index 511acb95b5..9157519a4a 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CompositeDownlinkRequest.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CompositeDownlinkRequest.java @@ -15,7 +15,7 @@ *******************************************************************************/ package org.eclipse.leshan.core.request; -import java.util.Collection; +import java.util.List; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.response.LwM2mResponse; @@ -26,7 +26,7 @@ public interface CompositeDownlinkRequest extends DownlinkRequest { /** - * @return the collection of node path targeted by the request. + * @return the list of node paths targeted by the request. */ - Collection getPaths(); + List getPaths(); } diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java index 9da454ca0b..4eb3746fef 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java @@ -13,6 +13,7 @@ * Contributors: * Sierra Wireless - initial API and implementation * Michał Wadowski (Orange) - Add Observe-Composite feature. + * Michał Wadowski (Orange) - Add Cancel Composite-Observation feature. *******************************************************************************/ package org.eclipse.leshan.server.californium.observation; @@ -55,21 +56,22 @@ public static SingleObservation createLwM2mObservation(Request request) { } return new SingleObservation(request.getToken().getBytes(), observeCommon.regId, observeCommon.lwm2mPath.get(0), - observeCommon.contentFormat, observeCommon.context); + observeCommon.responseContentFormat, observeCommon.context); } public static CompositeObservation createLwM2mCompositeObservation(Request request) { ObserveCommon observeCommon = new ObserveCommon(request); return new CompositeObservation(request.getToken().getBytes(), observeCommon.regId, observeCommon.lwm2mPath, - observeCommon.contentFormat, observeCommon.context); + observeCommon.requestContentFormat, observeCommon.responseContentFormat, observeCommon.context); } private static class ObserveCommon { String regId; Map context; List lwm2mPath; - ContentFormat contentFormat; + ContentFormat requestContentFormat; + ContentFormat responseContentFormat; public ObserveCommon(Request request) { if (request.getUserContext() == null) { @@ -100,8 +102,12 @@ public ObserveCommon(Request request) { throw new IllegalStateException("missing path in request context"); } + if (request.getOptions().hasContentFormat()) { + requestContentFormat = ContentFormat.fromCode(request.getOptions().getContentFormat()); + } + if (request.getOptions().hasAccept()) { - contentFormat = ContentFormat.fromCode(request.getOptions().getAccept()); + responseContentFormat = ContentFormat.fromCode(request.getOptions().getAccept()); } } } diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java index 4994b3f939..2c741b583c 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java @@ -177,8 +177,8 @@ public void visit(CancelObservationRequest request) { coapRequest = Request.newGet(); coapRequest.setObserveCancel(); coapRequest.setToken(request.getObservation().getId()); - if (request.getObservation().getContentFormat() != null) - coapRequest.getOptions().setAccept(request.getObservation().getContentFormat().getCode()); + if (request.getContentFormat() != null) + coapRequest.getOptions().setAccept(request.getContentFormat().getCode()); setTarget(coapRequest, request.getPath()); applyLowerLayerConfig(coapRequest); } diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java index ad2f363d63..062de7ba80 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java @@ -12,6 +12,7 @@ * * Contributors: * Michał Wadowski (Orange) - Add Observe-Composite feature. + * Michał Wadowski (Orange) - Add Cancel Composite-Observation feature. *******************************************************************************/ package org.eclipse.leshan.server.californium.observation; @@ -82,7 +83,8 @@ public void should_create_composite_observation_from_context() { Request coapRequest = new Request(null); coapRequest.setUserContext(userContext); coapRequest.setToken(exampleToken); - coapRequest.getOptions().setAccept(ContentFormat.DEFAULT.getCode()); + coapRequest.getOptions().setContentFormat(ContentFormat.CBOR.getCode()); + coapRequest.getOptions().setAccept(ContentFormat.JSON.getCode()); CompositeObservation observation = ObserveUtil.createLwM2mCompositeObservation(coapRequest); @@ -92,7 +94,8 @@ public void should_create_composite_observation_from_context() { assertEquals(exampleToken.getBytes(), observation.getId()); assertTrue(observation.getContext().containsKey("extraKey")); assertEquals("extraValue", observation.getContext().get("extraKey")); - assertEquals(ContentFormat.DEFAULT, observation.getContentFormat()); + assertEquals(ContentFormat.CBOR, observation.getRequestContentFormat()); + assertEquals(ContentFormat.JSON, observation.getResponseContentFormat()); } @Test From 49a317641396acdb42661ed4898c72250d08e0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wadowski?= Date: Fri, 30 Jul 2021 09:36:31 +0200 Subject: [PATCH 6/8] Add active Cancel Composite-Observation feature --- .../CancelCompositeObservationRequest.java | 73 +++++++++++++++++++ .../DownLinkRequestVisitorAdapter.java | 5 ++ .../core/request/DownlinkRequestVisitor.java | 3 + .../CancelCompositeObservationResponse.java | 41 +++++++++++ .../request/CoapRequestBuilder.java | 22 +++++- .../request/LwM2mResponseBuilder.java | 21 ++++++ 6 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelCompositeObservationRequest.java create mode 100644 leshan-core/src/main/java/org/eclipse/leshan/core/response/CancelCompositeObservationResponse.java diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelCompositeObservationRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelCompositeObservationRequest.java new file mode 100644 index 0000000000..8a79cb73aa --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelCompositeObservationRequest.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 2021 Orange. + * + * 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: + * Michał Wadowski (Orange) - Add Cancel Composite-Observation feature. + *******************************************************************************/ +package org.eclipse.leshan.core.request; + +import java.util.List; + +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.response.CancelCompositeObservationResponse; +import org.eclipse.leshan.core.util.Hex; + +/** + * A Lightweight M2M request for actively cancel an composite-observation. + *

+ * At server side this will not remove the observation from the observation store, to do it you need to use + * {@code ObservationService#cancelObservation()} + *

+ */ +public class CancelCompositeObservationRequest extends AbstractLwM2mRequest + implements CompositeDownlinkRequest { + + private final CompositeObservation observation; + + /** + * @param observation the observation to cancel actively + */ + public CancelCompositeObservationRequest(CompositeObservation observation) { + super(null); + this.observation = observation; + } + + @Override + public void accept(DownlinkRequestVisitor visitor) { + visitor.visit(this); + } + + public Observation getObservation() { + return observation; + } + + @Override + public final String toString() { + return String.format("CancelCompositeObservation [paths=%s token=%s]", getPaths(), + Hex.encodeHexString(observation.getId())); + } + + @Override + public List getPaths() { + return observation.getPaths(); + } + + public ContentFormat getRequestContentFormat() { + return observation.getRequestContentFormat(); + } + + public ContentFormat getResponseContentFormat() { + return observation.getResponseContentFormat(); + } +} diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownLinkRequestVisitorAdapter.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownLinkRequestVisitorAdapter.java index e10e28ec24..c1ac4d4ee4 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownLinkRequestVisitorAdapter.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownLinkRequestVisitorAdapter.java @@ -89,4 +89,9 @@ public void visit(BootstrapFinishRequest request) { @Override public void visit(ObserveCompositeRequest request) { } + + @Override + public void visit(CancelCompositeObservationRequest request) { + + } } diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownlinkRequestVisitor.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownlinkRequestVisitor.java index 2a999c3a3c..4d92999d52 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownlinkRequestVisitor.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/DownlinkRequestVisitor.java @@ -13,6 +13,7 @@ * Contributors: * Sierra Wireless - initial API and implementation * Michał Wadowski (Orange) - Add Observe-Composite feature. + * Michał Wadowski (Orange) - Add Cancel Composite-Observation feature. *******************************************************************************/ package org.eclipse.leshan.core.request; @@ -42,6 +43,8 @@ public interface DownlinkRequestVisitor { void visit(ObserveCompositeRequest request); + void visit(CancelCompositeObservationRequest request); + void visit(WriteCompositeRequest writeCompositeRequest); void visit(BootstrapDiscoverRequest request); diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/response/CancelCompositeObservationResponse.java b/leshan-core/src/main/java/org/eclipse/leshan/core/response/CancelCompositeObservationResponse.java new file mode 100644 index 0000000000..7028a48919 --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/response/CancelCompositeObservationResponse.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2021 Orange. + * + * 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: + * Michał Wadowski (Orange) - Add Cancel Composite-Observation feature. + *******************************************************************************/ +package org.eclipse.leshan.core.response; + +import java.util.Map; + +import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.observation.CompositeObservation; + +public class CancelCompositeObservationResponse extends ObserveCompositeResponse { + + public CancelCompositeObservationResponse(ResponseCode code, Map content, String errorMessage, + Object coapResponse, CompositeObservation observation) { + super(code, content, errorMessage, coapResponse, observation); + } + + @Override + public String toString() { + if (errorMessage != null) { + return String.format("CancelCompositeObservationResponse [code=%s, errormessage=%s]", code, errorMessage); + } else { + return String.format("CancelCompositeObservationResponse [code=%s, content=%s, observation=%s]", code, + content, observation); + } + } +} diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java index 2c741b583c..bbf525009b 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java @@ -16,6 +16,7 @@ * and transform them to * EndpointContext for requests * Michał Wadowski (Orange) - Add Observe-Composite feature. + * Michał Wadowski (Orange) - Add Cancel Composite-Observation feature. *******************************************************************************/ package org.eclipse.leshan.server.californium.request; @@ -34,6 +35,7 @@ import org.eclipse.leshan.core.request.BootstrapFinishRequest; import org.eclipse.leshan.core.request.BootstrapReadRequest; import org.eclipse.leshan.core.request.BootstrapWriteRequest; +import org.eclipse.leshan.core.request.CancelCompositeObservationRequest; import org.eclipse.leshan.core.request.CancelObservationRequest; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.CreateRequest; @@ -209,7 +211,25 @@ public void visit(ObserveCompositeRequest request) { coapRequest.setObserve(); setTarget(coapRequest, LwM2mPath.ROOTPATH); - coapRequest.setUserContext(ObserveUtil.createCoapObserveCompositeRequestContext(endpoint, registrationId, request)); + coapRequest.setUserContext( + ObserveUtil.createCoapObserveCompositeRequestContext(endpoint, registrationId, request)); + applyLowerLayerConfig(coapRequest); + } + + @Override + public void visit(CancelCompositeObservationRequest request) { + coapRequest = Request.newFetch(); + coapRequest.setObserveCancel(); + coapRequest.setToken(request.getObservation().getId()); + + coapRequest.getOptions().setContentFormat(request.getRequestContentFormat().getCode()); + coapRequest.setPayload(encoder.encodePaths(request.getPaths(), request.getRequestContentFormat())); + if (request.getResponseContentFormat() != null) { + coapRequest.getOptions().setAccept(request.getResponseContentFormat().getCode()); + } + + setTarget(coapRequest, LwM2mPath.ROOTPATH); + applyLowerLayerConfig(coapRequest); } diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java index c0ca882800..e1237c04a0 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java @@ -13,6 +13,7 @@ * Contributors: * Sierra Wireless - initial API and implementation * Michał Wadowski (Orange) - Add Observe-Composite feature. + * Michał Wadowski (Orange) - Add Cancel Composite-Observation feature. *******************************************************************************/ package org.eclipse.leshan.server.californium.request; @@ -38,6 +39,7 @@ import org.eclipse.leshan.core.request.BootstrapFinishRequest; import org.eclipse.leshan.core.request.BootstrapReadRequest; import org.eclipse.leshan.core.request.BootstrapWriteRequest; +import org.eclipse.leshan.core.request.CancelCompositeObservationRequest; import org.eclipse.leshan.core.request.CancelObservationRequest; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.CreateRequest; @@ -59,6 +61,7 @@ import org.eclipse.leshan.core.response.BootstrapFinishResponse; import org.eclipse.leshan.core.response.BootstrapReadResponse; import org.eclipse.leshan.core.response.BootstrapWriteResponse; +import org.eclipse.leshan.core.response.CancelCompositeObservationResponse; import org.eclipse.leshan.core.response.CancelObservationResponse; import org.eclipse.leshan.core.response.CreateResponse; import org.eclipse.leshan.core.response.DeleteResponse; @@ -305,6 +308,24 @@ public void visit(ObserveCompositeRequest request) { } } + @Override + public void visit(CancelCompositeObservationRequest request) { + if (coapResponse.isError()) { + // handle error response: + lwM2mresponse = new CancelCompositeObservationResponse(toLwM2mResponseCode(coapResponse.getCode()), null, + coapResponse.getPayloadString(), coapResponse, null); + } else if (isResponseCodeContent() || isResponseCodeChanged()) { + // handle success response: + Map content = decodeCompositeCoapResponse(request.getPaths(), coapResponse, request, + clientEndpoint); + lwM2mresponse = new CancelCompositeObservationResponse(toLwM2mResponseCode(coapResponse.getCode()), content, + null, coapResponse, null); + } else { + // handle unexpected response: + handleUnexpectedResponseCode(clientEndpoint, request, coapResponse); + } + } + @Override public void visit(WriteCompositeRequest request) { if (coapResponse.isError()) { From 26ef844a54a0da367d6b93db58b03be216cc3d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wadowski?= Date: Thu, 19 Aug 2021 18:22:35 +0200 Subject: [PATCH 7/8] Add integration tests about Cancel Observe Composite --- .../tests/observe/ObserveCompositeTest.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTest.java index 20b9dd6a7b..862ca50abf 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveCompositeTest.java @@ -31,11 +31,13 @@ import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.request.CancelCompositeObservationRequest; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ReadRequest; import org.eclipse.leshan.core.request.WriteCompositeRequest; import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.response.CancelCompositeObservationResponse; import org.eclipse.leshan.core.response.LwM2mResponse; import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ReadResponse; @@ -342,4 +344,87 @@ public void can_observe_object() throws InterruptedException { assertEquals(readResp.getContent(), content.get(new LwM2mPath("/3"))); } + @Test + public void can_passive_cancel_composite_observation() throws InterruptedException { + // Send ObserveCompositeRequest + ObserveCompositeResponse observeCompositeResponse = helper.server.send(currentRegistration, + new ObserveCompositeRequest(ContentFormat.SENML_JSON, ContentFormat.SENML_JSON, "/3/0/15")); + + CompositeObservation observation = observeCompositeResponse.getObservation(); + + // Write single example value + LwM2mResponse writeResponse = helper.server + .send(helper.getCurrentRegistration(), new WriteRequest(3, 0, 15, "Europe/Paris")); + listener.waitForNotification(2000); + assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); + + // cancel observation : passive way + helper.server.getObservationService().cancelObservation(observation); + Set observations = + helper.server.getObservationService().getObservations(helper.getCurrentRegistration()); + assertTrue("Observation should be removed", observations.isEmpty()); + + // write device timezone + listener.reset(); + + // Write single value + writeResponse = helper.server + .send(helper.getCurrentRegistration(), new WriteRequest(3, 0, 15, "Europe/London")); + listener.waitForNotification(2000); + assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); + + assertFalse("Observation should be cancelled", listener.receivedNotify().get()); + } + + @Test + public void can_active_cancel_composite_observation() throws InterruptedException { + // Send ObserveCompositeRequest + ObserveCompositeResponse observeCompositeResponse = helper.server.send(currentRegistration, + new ObserveCompositeRequest(ContentFormat.SENML_JSON, ContentFormat.SENML_JSON, "/3/0/15")); + + CompositeObservation observation = observeCompositeResponse.getObservation(); + + // Write single example value + LwM2mResponse writeResponse = helper.server + .send(helper.getCurrentRegistration(), new WriteRequest(3, 0, 15, "Europe/Paris")); + listener.waitForNotification(2000); + assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); + + // cancel observation : active way + CancelCompositeObservationResponse response = helper.server.send(helper.getCurrentRegistration(), + new CancelCompositeObservationRequest(observation)); + assertTrue(response.isSuccess()); + assertEquals(ResponseCode.CONTENT, response.getCode()); + + // Assert that response contains exactly the same paths + assertTrue(listener.receivedNotify().get()); + Map content = listener.getObserveCompositeResponse().getContent(); + assertEquals(1, content.size()); + assertTrue(content.containsKey(new LwM2mPath("/3/0/15"))); + + // Assert that listener response contains exact values + assertEquals(LwM2mSingleResource.newStringResource(new LwM2mPath("/3/0/15").getResourceId(), "Europe/Paris"), + content.get(new LwM2mPath("/3/0/15"))); + + // active cancellation does not remove observation from store : it should be done manually using + // ObservationService().cancelObservation(observation) + + // Assert that there is one valid observation + assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId()); + Set observations = helper.server.getObservationService() + .getObservations(helper.getCurrentRegistration()); + assertEquals("We should have only one observation", 1, observations.size()); + assertTrue("New observation is not there", observations.contains(observation)); + + // Write device timezone + listener.reset(); + + writeResponse = helper.server + .send(helper.getCurrentRegistration(), new WriteRequest(3, 0, 15, "Europe/London")); + listener.waitForNotification(2000); + assertEquals(ResponseCode.CHANGED, writeResponse.getCode()); + + assertFalse("Observation should be cancelled", listener.receivedNotify().get()); + } + } From ab12d0857574b49f8c0e5b6dc2c8c2218108e0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wadowski?= Date: Tue, 31 Aug 2021 13:36:03 +0200 Subject: [PATCH 8/8] Reduce decoding overhead with passing path list via user context in ObserveCompositeRelationFilter --- .../ObserveCompositeRelationFilter.java | 17 +---- .../client/californium/RootResource.java | 15 +++- .../leshan/core/californium}/ObserveUtil.java | 73 ++++++++++++------- .../redis/RedisRegistrationStoreTest.java | 2 +- .../observation/ObservationServiceImpl.java | 1 + .../InMemoryRegistrationStore.java | 2 +- .../request/CoapRequestBuilder.java | 2 +- .../request/LwM2mResponseBuilder.java | 2 +- .../observation/ObservationServiceTest.java | 1 + .../observation/ObserveUtilTest.java | 1 + .../InMemoryRegistrationStoreTest.java | 2 +- .../request/LwM2mResponseBuilderTest.java | 2 +- .../server/redis/RedisRegistrationStore.java | 2 +- 13 files changed, 75 insertions(+), 47 deletions(-) rename {leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation => leshan-core-cf/src/main/java/org/eclipse/leshan/core/californium}/ObserveUtil.java (80%) diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/ObserveCompositeRelationFilter.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/ObserveCompositeRelationFilter.java index dc1559cf99..4308a18902 100644 --- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/ObserveCompositeRelationFilter.java +++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/ObserveCompositeRelationFilter.java @@ -22,27 +22,22 @@ import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.observe.ObserveRelationFilter; +import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.node.LwM2mPath; -import org.eclipse.leshan.core.node.codec.CodecException; -import org.eclipse.leshan.core.node.codec.LwM2mDecoder; -import org.eclipse.leshan.core.request.ContentFormat; /** * An {@link ObserveRelationFilter} which select {@link ObserveRelation} based on one of resource URIs. */ class ObserveCompositeRelationFilter implements ObserveRelationFilter { - private final LwM2mDecoder decoder; private final List paths; /** * Instantiates {@link ObserveCompositeRelationFilter} basing on resource URIs. * - * @param decoder {@link LwM2mDecoder} which will decode data in supported content format. * @param paths the list of {@link LwM2mPath} */ - public ObserveCompositeRelationFilter(LwM2mDecoder decoder, LwM2mPath... paths) { - this.decoder = decoder; + public ObserveCompositeRelationFilter(LwM2mPath... paths) { this.paths = Arrays.asList(paths); } @@ -84,12 +79,6 @@ private Request getRequest(ObserveRelation relation) { } private List getObserveRequestPaths(Request request) { - ContentFormat contentFormat = ContentFormat.fromCode(request.getOptions().getContentFormat()); - - try { - return decoder.decodePaths(request.getPayload(), contentFormat); - } catch (CodecException e) { - return null; - } + return ObserveUtil.getPathsFromContext(request.getUserContext()); } } diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/RootResource.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/RootResource.java index 3a596bd57f..1732f876d3 100644 --- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/RootResource.java +++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/RootResource.java @@ -19,6 +19,7 @@ import static org.eclipse.leshan.core.californium.ResponseCodeUtil.toCoapResponseCode; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,6 +35,7 @@ import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter; import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.Link; +import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.codec.LwM2mDecoder; @@ -131,6 +133,8 @@ public void handleFETCH(CoapExchange exchange) { responseContentFormat, paths, coapRequest); ObserveCompositeResponse response = rootEnabler.observe(identity, observeRequest); + updateUserContextWithPaths(coapRequest, paths); + if (response.getCode().isError()) { exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage()); return; @@ -157,6 +161,15 @@ public void handleFETCH(CoapExchange exchange) { } } + private void updateUserContextWithPaths(Request coapRequest, List paths) { + HashMap userContext = new HashMap<>(); + if (coapRequest.getUserContext() != null) { + userContext.putAll(coapRequest.getUserContext()); + } + ObserveUtil.addPathsIntoContext(userContext, paths); + coapRequest.setUserContext(userContext); + } + @Override public void handleIPATCH(CoapExchange exchange) { ServerIdentity identity = getServerOrRejectRequest(exchange); @@ -209,7 +222,7 @@ private void addListeners() { @Override public void resourceChanged(LwM2mPath... paths) { - changed(new ObserveCompositeRelationFilter(decoder, paths)); + changed(new ObserveCompositeRelationFilter(paths)); } }); } diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java b/leshan-core-cf/src/main/java/org/eclipse/leshan/core/californium/ObserveUtil.java similarity index 80% rename from leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java rename to leshan-core-cf/src/main/java/org/eclipse/leshan/core/californium/ObserveUtil.java index 4eb3746fef..2ec6799984 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObserveUtil.java +++ b/leshan-core-cf/src/main/java/org/eclipse/leshan/core/californium/ObserveUtil.java @@ -15,9 +15,10 @@ * Michał Wadowski (Orange) - Add Observe-Composite feature. * Michał Wadowski (Orange) - Add Cancel Composite-Observation feature. *******************************************************************************/ -package org.eclipse.leshan.server.californium.observation; +package org.eclipse.leshan.core.californium; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,11 +32,9 @@ import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ObserveRequest; -import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; /** - * Utility functions to help to handle observation in Leshan. Those helper functions are only needed if you're - * implementing your own {@link CaliforniumRegistrationStore}. + * Utility functions to help to handle observation in Leshan. */ public class ObserveUtil { @@ -50,26 +49,26 @@ public class ObserveUtil { public static SingleObservation createLwM2mObservation(Request request) { ObserveCommon observeCommon = new ObserveCommon(request); - if (observeCommon.lwm2mPath.size() != 1) { + if (observeCommon.lwm2mPaths.size() != 1) { throw new IllegalStateException( - "1 path is expected in observe request context but was " + observeCommon.lwm2mPath); + "1 path is expected in observe request context but was " + observeCommon.lwm2mPaths); } - return new SingleObservation(request.getToken().getBytes(), observeCommon.regId, observeCommon.lwm2mPath.get(0), - observeCommon.responseContentFormat, observeCommon.context); + return new SingleObservation(request.getToken().getBytes(), observeCommon.regId, + observeCommon.lwm2mPaths.get(0), observeCommon.responseContentFormat, observeCommon.context); } public static CompositeObservation createLwM2mCompositeObservation(Request request) { ObserveCommon observeCommon = new ObserveCommon(request); - return new CompositeObservation(request.getToken().getBytes(), observeCommon.regId, observeCommon.lwm2mPath, + return new CompositeObservation(request.getToken().getBytes(), observeCommon.regId, observeCommon.lwm2mPaths, observeCommon.requestContentFormat, observeCommon.responseContentFormat, observeCommon.context); } private static class ObserveCommon { String regId; Map context; - List lwm2mPath; + List lwm2mPaths; ContentFormat requestContentFormat; ContentFormat responseContentFormat; @@ -78,7 +77,6 @@ public ObserveCommon(Request request) { throw new IllegalStateException("missing request context"); } - lwm2mPath = new ArrayList<>(); context = new HashMap<>(); for (Entry ctx : request.getUserContext().entrySet()) { @@ -87,9 +85,7 @@ public ObserveCommon(Request request) { regId = ctx.getValue(); break; case CTX_LWM2M_PATH: - for (String path : ctx.getValue().split("\n")) { - lwm2mPath.add(new LwM2mPath(path)); - } + lwm2mPaths = getPathsFromContext(request.getUserContext()); break; case CTX_ENDPOINT: break; @@ -98,7 +94,7 @@ public ObserveCommon(Request request) { } } - if (lwm2mPath.size() == 0) { + if (lwm2mPaths == null || lwm2mPaths.size() == 0) { throw new IllegalStateException("missing path in request context"); } @@ -112,6 +108,25 @@ public ObserveCommon(Request request) { } } + /** + * Extract {@link LwM2mPath} list from encoded information in user context. + * + * @param userContext user context + * @return the list of {@link LwM2mPath} + */ + public static List getPathsFromContext(Map userContext) { + if (userContext.containsKey(CTX_LWM2M_PATH)) { + List lwm2mPaths = new ArrayList<>(); + String pathsEncoded = userContext.get(CTX_LWM2M_PATH); + + for (String path : pathsEncoded.split("\n")) { + lwm2mPaths.add(new LwM2mPath(path)); + } + return lwm2mPaths; + } + return null; + } + /** * Create a CoAP observe request context with specific keys needed for internal Leshan working. */ @@ -120,10 +135,10 @@ public static Map createCoapObserveRequestContext(String endpoin Map context = new HashMap<>(); context.put(CTX_ENDPOINT, endpoint); context.put(CTX_REGID, registrationId); - context.put(CTX_LWM2M_PATH, request.getPath().toString()); - for (Entry ctx : request.getContext().entrySet()) { - context.put(ctx.getKey(), ctx.getValue()); - } + + addPathsIntoContext(context, Collections.singletonList(request.getPath())); + + context.putAll(request.getContext()); return context; } @@ -133,17 +148,25 @@ public static Map createCoapObserveCompositeRequestContext(Strin context.put(CTX_ENDPOINT, endpoint); context.put(CTX_REGID, registrationId); + addPathsIntoContext(context, request.getPaths()); + + context.putAll(request.getContext()); + return context; + } + + /** + * Update user context with encoded list of {@link LwM2mPath}. + * + * @param context user context + * @param paths the list of {@link LwM2mPath} + */ + public static void addPathsIntoContext(Map context, List paths) { StringBuilder sb = new StringBuilder(); - for (LwM2mPath path : request.getPaths()) { + for (LwM2mPath path : paths) { sb.append(path.toString()); sb.append("\n"); } - context.put(CTX_LWM2M_PATH, sb.toString()); - for (Entry ctx : request.getContext().entrySet()) { - context.put(ctx.getKey(), ctx.getValue()); - } - return context; } public static String extractRegistrationId(org.eclipse.californium.core.observe.Observation observation) { diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/server/redis/RedisRegistrationStoreTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/server/redis/RedisRegistrationStoreTest.java index e46c3fed85..0a6ba5b600 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/server/redis/RedisRegistrationStoreTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/server/redis/RedisRegistrationStoreTest.java @@ -31,6 +31,7 @@ import org.eclipse.californium.core.coap.Token; import org.eclipse.californium.elements.AddressEndpointContext; import org.eclipse.leshan.core.Link; +import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; @@ -41,7 +42,6 @@ import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.integration.tests.util.RedisIntegrationTestHelper; -import org.eclipse.leshan.server.californium.observation.ObserveUtil; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; import org.eclipse.leshan.server.redis.RedisRegistrationStore; import org.eclipse.leshan.server.registration.Registration; diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObservationServiceImpl.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObservationServiceImpl.java index a1de3f1d45..61630a93ee 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObservationServiceImpl.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/observation/ObservationServiceImpl.java @@ -35,6 +35,7 @@ import org.eclipse.californium.core.observe.ObservationStore; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.californium.EndpointContextUtil; +import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java index 9e1177633a..4f5a5f31eb 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStore.java @@ -51,12 +51,12 @@ import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Stoppable; +import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.Identity; import org.eclipse.leshan.core.util.NamedThreadFactory; -import org.eclipse.leshan.server.californium.observation.ObserveUtil; import org.eclipse.leshan.server.registration.Deregistration; import org.eclipse.leshan.server.registration.ExpirationListener; import org.eclipse.leshan.server.registration.Registration; diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java index bbf525009b..7220fb635f 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java @@ -24,6 +24,7 @@ import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.elements.EndpointContext; import org.eclipse.leshan.core.californium.EndpointContextUtil; +import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mObject; @@ -53,7 +54,6 @@ import org.eclipse.leshan.core.request.WriteCompositeRequest; import org.eclipse.leshan.core.request.WriteRequest; import org.eclipse.leshan.core.util.StringUtils; -import org.eclipse.leshan.server.californium.observation.ObserveUtil; import org.eclipse.leshan.server.request.LowerLayerConfig; /** diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java index e1237c04a0..c1e2975ed3 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java @@ -27,6 +27,7 @@ import org.eclipse.californium.core.coap.Response; import org.eclipse.leshan.core.Link; import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; @@ -76,7 +77,6 @@ import org.eclipse.leshan.core.response.WriteCompositeResponse; import org.eclipse.leshan.core.response.WriteResponse; import org.eclipse.leshan.core.util.Hex; -import org.eclipse.leshan.server.californium.observation.ObserveUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObservationServiceTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObservationServiceTest.java index 6666651a41..862c02b134 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObservationServiceTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObservationServiceTest.java @@ -27,6 +27,7 @@ import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.eclipse.leshan.core.californium.EndpointContextUtil; +import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; import org.eclipse.leshan.core.observation.CompositeObservation; diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java index 062de7ba80..aaada9aef2 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/observation/ObserveUtilTest.java @@ -25,6 +25,7 @@ import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Token; +import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.SingleObservation; diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStoreTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStoreTest.java index ee69e48f72..0ba11de277 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStoreTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/registration/InMemoryRegistrationStoreTest.java @@ -30,6 +30,7 @@ import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Token; import org.eclipse.leshan.core.Link; +import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; @@ -39,7 +40,6 @@ import org.eclipse.leshan.core.request.Identity; import org.eclipse.leshan.core.request.ObserveCompositeRequest; import org.eclipse.leshan.core.request.ObserveRequest; -import org.eclipse.leshan.server.californium.observation.ObserveUtil; import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.registration.RegistrationUpdate; import org.eclipse.leshan.server.registration.UpdatedRegistration; diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilderTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilderTest.java index c2aee3500a..e9aab00fc3 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilderTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilderTest.java @@ -25,6 +25,7 @@ import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.coap.Token; +import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.SingleObservation; @@ -33,7 +34,6 @@ import org.eclipse.leshan.core.response.ObserveCompositeResponse; import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.server.californium.DummyDecoder; -import org.eclipse.leshan.server.californium.observation.ObserveUtil; import org.junit.Test; public class LwM2mResponseBuilderTest { diff --git a/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java b/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java index bebc2f49dd..7b318eb027 100644 --- a/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java +++ b/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/RedisRegistrationStore.java @@ -43,13 +43,13 @@ import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.Startable; import org.eclipse.leshan.core.Stoppable; +import org.eclipse.leshan.core.californium.ObserveUtil; import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.observation.SingleObservation; import org.eclipse.leshan.core.request.Identity; import org.eclipse.leshan.core.util.NamedThreadFactory; import org.eclipse.leshan.core.util.Validate; -import org.eclipse.leshan.server.californium.observation.ObserveUtil; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; import org.eclipse.leshan.server.redis.serialization.IdentitySerDes; import org.eclipse.leshan.server.redis.serialization.ObservationSerDes;