From 633e3e2ea7322caebd769b213de2ca2d65c30986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wadowski?= Date: Wed, 2 Mar 2022 10:12:50 +0100 Subject: [PATCH] #1218: Add Timestamped nodes support to Send Operation at server side. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: MichaƂ Wadowski Also-by: Simon Bernard --- .../request/CoapRequestBuilder.java | 2 +- .../leshan/core/node/LwM2mNodeUtil.java | 44 ++++ .../core/node/TimestampedLwM2mNodes.java | 199 ++++++++++++++++ .../core/node/codec/DefaultLwM2mDecoder.java | 26 +++ .../core/node/codec/DefaultLwM2mEncoder.java | 18 ++ .../leshan/core/node/codec/LwM2mDecoder.java | 14 ++ .../leshan/core/node/codec/LwM2mEncoder.java | 14 ++ .../codec/TimestampedMultiNodeDecoder.java | 38 +++ .../codec/senml/LwM2mNodeSenMLDecoder.java | 28 ++- .../leshan/core/request/SendRequest.java | 35 ++- .../core/node/TimestampedLwM2mNodesTest.java | 221 ++++++++++++++++++ .../core/node/codec/LwM2mNodeDecoderTest.java | 26 +++ .../integration/tests/send/SendTest.java | 13 +- .../tests/send/SendTimestampedTest.java | 156 +++++++++++++ .../tests/util/SynchronousSendListener.java | 12 +- .../server/californium/send/SendResource.java | 9 +- .../server/californium/DummyDecoder.java | 7 + .../leshan/server/send/SendHandler.java | 18 +- .../leshan/server/send/SendListener.java | 6 +- .../server/demo/servlet/EventServlet.java | 6 +- 20 files changed, 835 insertions(+), 57 deletions(-) create mode 100644 leshan-core/src/main/java/org/eclipse/leshan/core/node/TimestampedLwM2mNodes.java create mode 100644 leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedMultiNodeDecoder.java create mode 100644 leshan-core/src/test/java/org/eclipse/leshan/core/node/TimestampedLwM2mNodesTest.java create mode 100644 leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTimestampedTest.java diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CoapRequestBuilder.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CoapRequestBuilder.java index 9a65831eae..2f58647030 100644 --- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CoapRequestBuilder.java +++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/request/CoapRequestBuilder.java @@ -162,7 +162,7 @@ public void visit(SendRequest request) { ContentFormat format = request.getFormat(); coapRequest.getOptions().setContentFormat(format.getCode()); - coapRequest.setPayload(encoder.encodeNodes(request.getNodes(), format, model)); + coapRequest.setPayload(encoder.encodeTimestampedNodes(request.getTimestampedNodes(), format, model)); } public Request getRequest() { diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeUtil.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeUtil.java index 85748d167f..18b2fd34b4 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeUtil.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeUtil.java @@ -266,4 +266,48 @@ public static void validateIncompletePath(LwM2mPath path) throws InvalidLwM2mPat if (err != null) throw new InvalidLwM2mPathException(err); } + + public static String getInvalidPathForNodeCause(LwM2mNode node, LwM2mPath path) { + if (node instanceof LwM2mObject) { + if (!path.isObject()) { + return String.format("Invalid Path %s : path does not target a LWM2M object for %s", path, node); + } else if (node.getId() != path.getObjectId()) { + return String.format("Invalid Path %s : path object id (%d) does not match LWM2M object id %d for %s", + path, path.getObjectId(), node.getId(), node); + } + } else if (node instanceof LwM2mObjectInstance) { + if (!path.isObjectInstance()) { + return String.format("Invalid Path %s : path does not target a LWM2M object instance for %s", path, + node); + } else if (node.getId() != path.getObjectInstanceId()) { + return String.format( + "Invalid Path %s : path object instance id (%d) does not match LWM2M object instance id %d for %s", + path, path.getObjectInstanceId(), node.getId(), node); + } + } else if (node instanceof LwM2mResource) { + if (!path.isResource()) { + return String.format("Invalid Path %s : path does not target a LWM2M resource for %s", path, node); + } else if (node.getId() != path.getResourceId()) { + return String.format( + "Invalid Path %s : path resource id (%d) does not match LWM2M resource id %d for %s", path, + path.getResourceId(), node.getId(), node); + } + } else if (node instanceof LwM2mResourceInstance) { + if (!path.isResourceInstance()) { + return String.format("Invalid Path %s : path does not target a LWM2M resource instance for %s", path, + node); + } else if (node.getId() != path.getResourceInstanceId()) { + return String.format( + "Invalid Path %s : path resource instance id (%d) does not match LWM2M resource instance id %d for %s", + path, path.getResourceInstanceId(), node.getId(), node); + } + } + return null; + } + + public static void validatePathForNode(LwM2mNode node, LwM2mPath path) throws InvalidLwM2mPathException { + String err = getInvalidPathForNodeCause(node, path); + if (err != null) + throw new InvalidLwM2mPathException(err); + } } diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/TimestampedLwM2mNodes.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/TimestampedLwM2mNodes.java new file mode 100644 index 0000000000..acfbf57083 --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/TimestampedLwM2mNodes.java @@ -0,0 +1,199 @@ +/******************************************************************************* + * 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: + * Orange - Send with multiple-timestamped values + *******************************************************************************/ +package org.eclipse.leshan.core.node; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; + +/** + * A container for nodes {@link LwM2mNode} with path {@link LwM2mPath} and optional timestamp information. + */ +public class TimestampedLwM2mNodes { + + private final Map> timestampedPathNodesMap; + + private TimestampedLwM2mNodes(Map> timestampedPathNodesMap) { + this.timestampedPathNodesMap = timestampedPathNodesMap; + } + + /** + * Get nodes for specific timestamp. Null timestamp is allowed. + * + * @return map of {@link LwM2mPath}-{@link LwM2mNode} or null if there is no value for asked timestamp. + */ + public Map getNodesAt(Long timestamp) { + Map map = timestampedPathNodesMap.get(timestamp); + if (map != null) { + return Collections.unmodifiableMap(timestampedPathNodesMap.get(timestamp)); + } + return null; + } + + /** + * Get all collected nodes as {@link LwM2mPath}-{@link LwM2mNode} map ignoring timestamp information. In case of the + * same path conflict the most recent one is taken. Null timestamp is considered as most recent one. + */ + public Map getNodes() { + Map result = new HashMap<>(); + for (Map.Entry> entry : timestampedPathNodesMap.entrySet()) { + result.putAll(entry.getValue()); + } + return Collections.unmodifiableMap(result); + } + + /** + * Returns the all sorted timestamps of contained nodes with ascending order. Null timestamp is considered as most + * recent one. + */ + public Set getTimestamps() { + return Collections.unmodifiableSet(timestampedPathNodesMap.keySet()); + } + + @Override + public String toString() { + return String.format("TimestampedLwM2mNodes [%s]", timestampedPathNodesMap); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((timestampedPathNodesMap == null) ? 0 : timestampedPathNodesMap.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TimestampedLwM2mNodes other = (TimestampedLwM2mNodes) obj; + if (timestampedPathNodesMap == null) { + if (other.timestampedPathNodesMap != null) + return false; + } else if (!timestampedPathNodesMap.equals(other.timestampedPathNodesMap)) + return false; + return true; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private static class InternalNode { + Long timestamp; + LwM2mPath path; + LwM2mNode node; + + public InternalNode(Long timestamp, LwM2mPath path, LwM2mNode node) { + this.timestamp = timestamp; + this.path = path; + this.node = node; + } + } + + private final List nodes = new ArrayList<>(); + private boolean noDuplicate = true; + + public Builder raiseExceptionOnDuplicate(boolean raiseException) { + noDuplicate = raiseException; + return this; + } + + public Builder addNodes(Map pathNodesMap) { + for (Entry node : pathNodesMap.entrySet()) { + nodes.add(new InternalNode(null, node.getKey(), node.getValue())); + } + return this; + } + + public Builder put(Long timestamp, LwM2mPath path, LwM2mNode node) { + nodes.add(new InternalNode(timestamp, path, node)); + return this; + } + + public Builder put(LwM2mPath path, LwM2mNode node) { + nodes.add(new InternalNode(null, path, node)); + return this; + } + + public Builder add(TimestampedLwM2mNodes timestampedNodes) { + for (Long timestamp : timestampedNodes.getTimestamps()) { + Map pathNodeMap = timestampedNodes.getNodesAt(timestamp); + for (Map.Entry pathNodeEntry : pathNodeMap.entrySet()) { + nodes.add(new InternalNode(timestamp, pathNodeEntry.getKey(), pathNodeEntry.getValue())); + } + } + return this; + } + + /** + * Build the {@link TimestampedLwM2mNodes} and raise {@link IllegalArgumentException} if builder inputs are + * invalid. + */ + public TimestampedLwM2mNodes build() throws IllegalArgumentException { + Map> timestampToPathToNode = new TreeMap<>(getTimestampComparator()); + + for (InternalNode internalNode : nodes) { + // validate path is consistent with Node + String cause = LwM2mNodeUtil.getInvalidPathForNodeCause(internalNode.node, internalNode.path); + if (cause != null) { + throw new IllegalArgumentException(cause); + } + + // add to the map + Map pathToNode = timestampToPathToNode.get(internalNode.timestamp); + if (pathToNode == null) { + pathToNode = new HashMap<>(); + timestampToPathToNode.put(internalNode.timestamp, pathToNode); + pathToNode.put(internalNode.path, internalNode.node); + } else { + LwM2mNode previous = pathToNode.put(internalNode.path, internalNode.node); + if (noDuplicate && previous != null) { + throw new IllegalArgumentException(String.format( + "Unable to create TimestampedLwM2mNodes : duplicate value for path %s. (%s, %s)", + internalNode.path, internalNode.node, previous)); + } + } + } + return new TimestampedLwM2mNodes(timestampToPathToNode); + } + + private static Comparator getTimestampComparator() { + return (o1, o2) -> { + if (o1 == null) { + return (o2 == null) ? 0 : 1; + } else if (o2 == null) { + return -1; + } else { + return o1.compareTo(o2); + } + }; + } + } +} \ No newline at end of file diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mDecoder.java index 93a902882f..72863312c7 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mDecoder.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mDecoder.java @@ -31,6 +31,7 @@ import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mResourceInstance; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.node.codec.cbor.LwM2mNodeCborDecoder; import org.eclipse.leshan.core.node.codec.json.LwM2mNodeJsonDecoder; import org.eclipse.leshan.core.node.codec.opaque.LwM2mNodeOpaqueDecoder; @@ -204,6 +205,31 @@ public List decodeTimestampedData(byte[] content, ContentF } } + @Override + public TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, ContentFormat format, LwM2mModel model) + throws CodecException { + LOG.trace("Decoding value for format {}: {}", format, content); + + if (format == null) { + throw new CodecException("Content format is mandatory."); + } + + NodeDecoder decoder = nodeDecoders.get(format); + if (decoder == null) { + throw new CodecException("Content format %s is not supported", format); + } + + if (decoder instanceof TimestampedMultiNodeDecoder) { + return ((TimestampedMultiNodeDecoder) decoder).decodeTimestampedNodes(content, model); + } else if (decoder instanceof MultiNodeDecoder) { + return new TimestampedLwM2mNodes.Builder() + .addNodes(((MultiNodeDecoder) decoder).decodeNodes(content, null, model)).build(); + } else { + throw new CodecException( + "Decoder does not support multiple nodes decoding for this content format %s [%s] ", format); + } + } + @Override public List decodePaths(byte[] content, ContentFormat format) throws CodecException { LOG.trace("Decoding paths encoded with {}: {}", format, content); diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mEncoder.java index 3b1ed67ad7..e5f3bfe8a0 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mEncoder.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mEncoder.java @@ -25,6 +25,7 @@ import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.node.codec.cbor.LwM2mNodeCborEncoder; import org.eclipse.leshan.core.node.codec.json.LwM2mNodeJsonEncoder; import org.eclipse.leshan.core.node.codec.opaque.LwM2mNodeOpaqueEncoder; @@ -199,6 +200,23 @@ public byte[] encodeTimestampedData(List timestampedNodes, } + @Override + public byte[] encodeTimestampedNodes(TimestampedLwM2mNodes timestampedNodes, ContentFormat format, + LwM2mModel model) throws CodecException { + Validate.notNull(timestampedNodes); + + if (format == null) { + throw new CodecException("Content format is mandatory."); + } + + NodeEncoder encoder = nodeEncoders.get(format); + if (encoder == null) { + throw new CodecException("Content format %s is not supported", format); + } + // TODO implements timestamped nodes encoding with fall back to "not timestamped" multi node. + return encodeNodes(timestampedNodes.getNodes(), format, model); + } + @Override public byte[] encodePaths(List paths, ContentFormat format) throws CodecException { Validate.notEmpty(paths); diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mDecoder.java index 00aa0abdda..28ae8d719c 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mDecoder.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mDecoder.java @@ -25,6 +25,7 @@ import org.eclipse.leshan.core.node.LwM2mResourceInstance; import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.request.ContentFormat; /** @@ -97,6 +98,19 @@ Map decodeNodes(byte[] content, ContentFormat format, List List decodeTimestampedData(byte[] content, ContentFormat format, LwM2mPath path, LwM2mModel model) throws CodecException; + /** + * Deserializes a binary content into a {@link TimestampedLwM2mNodes}. + *

+ * + * @param content the content + * @param format the content format + * @param model the collection of supported object models + * @return the decoded timestamped nodes represented by {@link TimestampedLwM2mNodes} + * @throws CodecException if content is malformed. + */ + TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, ContentFormat format, LwM2mModel model) + throws CodecException; + /** * Deserializes a binary content into a list of {@link LwM2mPath}. * diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mEncoder.java index 2441ac5081..20e97ce5e3 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mEncoder.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mEncoder.java @@ -23,6 +23,7 @@ import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.request.ContentFormat; /** @@ -73,6 +74,19 @@ public interface LwM2mEncoder { byte[] encodeTimestampedData(List timestampedNodes, ContentFormat format, LwM2mPath path, LwM2mModel model) throws CodecException; + /** + * Serializes a multiple time-stamped nodes contained in {@link TimestampedLwM2mNodes} with the given content + * format. + * + * @param data the {@link TimestampedLwM2mNodes} to serialize + * @param format the content format + * @param model the collection of supported object models + * @return the encoded node as a byte array + * @throws CodecException if encoding failed. + */ + byte[] encodeTimestampedNodes(TimestampedLwM2mNodes data, ContentFormat format, LwM2mModel model) + throws CodecException; + /** * Serializes a list of {@link LwM2mPath} with the given content format. * diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedMultiNodeDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedMultiNodeDecoder.java new file mode 100644 index 0000000000..dba265c54b --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedMultiNodeDecoder.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * 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: + * Orange - Send with multiple-timestamped values + *******************************************************************************/ +package org.eclipse.leshan.core.node.codec; + +import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; + +/** + * A decoder for {@link TimestampedLwM2mNodes}. + * + * @see DefaultLwM2mDecoder + */ +public interface TimestampedMultiNodeDecoder { + /** + * Deserializes a binary content into a {@link TimestampedLwM2mNodes}. + *

+ * + * @param content the content + * @param model the collection of supported object models + * @return the decoded timestamped nodes represented by {@link TimestampedLwM2mNodes} + * @throws CodecException if content is malformed. + */ + TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, LwM2mModel model) throws CodecException; + +} diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java index d269506e1a..9ac12d11cd 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLDecoder.java @@ -41,9 +41,11 @@ import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.node.ObjectLink; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; import org.eclipse.leshan.core.node.codec.MultiNodeDecoder; +import org.eclipse.leshan.core.node.codec.TimestampedMultiNodeDecoder; import org.eclipse.leshan.core.node.codec.TimestampedNodeDecoder; import org.eclipse.leshan.core.util.Hex; import org.eclipse.leshan.core.util.datatype.NumberUtil; @@ -55,7 +57,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class LwM2mNodeSenMLDecoder implements TimestampedNodeDecoder, MultiNodeDecoder { +public class LwM2mNodeSenMLDecoder implements TimestampedNodeDecoder, MultiNodeDecoder, TimestampedMultiNodeDecoder { private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeSenMLDecoder.class); @@ -177,6 +179,30 @@ public List decodeTimestampedData(byte[] content, LwM2mPat } } + @Override + public TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, LwM2mModel model) throws CodecException { + try { + // Decode SenML pack + SenMLPack pack = decoder.fromSenML(content); + + TimestampedLwM2mNodes.Builder nodes = TimestampedLwM2mNodes.builder(); + + LwM2mSenMLResolver resolver = new LwM2mSenMLResolver(); + for (SenMLRecord record : pack.getRecords()) { + LwM2mResolvedSenMLRecord resolvedRecord = resolver.resolve(record); + LwM2mPath path = resolvedRecord.getPath(); + LwM2mNode node = parseRecords(Arrays.asList(resolvedRecord), path, model, + DefaultLwM2mDecoder.nodeClassFromPath(path)); + nodes.put(resolvedRecord.getTimeStamp(), path, node); + } + + return nodes.build(); + } catch (SenMLException e) { + String hexValue = content != null ? Hex.encodeHexString(content) : ""; + throw new CodecException(e, "Unable to decode nodes : %s", hexValue, e); + } + } + /** * Parse records for a given LWM2M path. */ diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/SendRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/SendRequest.java index 7417b35d9d..54efc2e214 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/SendRequest.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/SendRequest.java @@ -24,6 +24,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.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.request.exception.InvalidRequestException; import org.eclipse.leshan.core.response.SendResponse; import org.eclipse.leshan.core.util.Validate; @@ -35,11 +36,10 @@ * The "Send" operation can be used by the LwM2M Client to report values for Resources and Resource Instances of LwM2M * Object Instance(s) to the LwM2M Server. */ -public class SendRequest implements UplinkRequest { +public class SendRequest extends AbstractLwM2mRequest implements UplinkRequest { private final ContentFormat format; - private final Map nodes; - private final Object coapRequest; + private final TimestampedLwM2mNodes timestampedNodes; /** * @param format {@link ContentFormat} used to encode data. It MUST be {@link ContentFormat#SENML_CBOR} or @@ -52,16 +52,20 @@ public SendRequest(ContentFormat format, Map nodes) { } public SendRequest(ContentFormat format, Map nodes, Object coapRequest) { + this(format, TimestampedLwM2mNodes.builder().addNodes(nodes).build(), coapRequest); + } + + public SendRequest(ContentFormat format, TimestampedLwM2mNodes timestampedNodes, Object coapRequest) { + super(coapRequest); + this.timestampedNodes = timestampedNodes; // Validate Format if (format == null || !(format.equals(ContentFormat.SENML_CBOR) || format.equals(ContentFormat.SENML_JSON))) { throw new InvalidRequestException("Content format MUST be SenML_CBOR or SenML_JSON but was " + format); } // Validate Nodes - validateNodes(nodes); + validateNodes(timestampedNodes.getNodes()); this.format = format; - this.nodes = nodes; - this.coapRequest = coapRequest; } private void validateNodes(Map nodes) { @@ -86,13 +90,8 @@ private void validateNodes(Map nodes) { } } - @Override - public Object getCoapRequest() { - return coapRequest; - } - - public Map getNodes() { - return nodes; + public TimestampedLwM2mNodes getTimestampedNodes() { + return timestampedNodes; } public ContentFormat getFormat() { @@ -106,7 +105,7 @@ public void accept(UplinkRequestVisitor visitor) { @Override public String toString() { - return String.format("SendRequest [format=%s, nodes=%s]", format, nodes); + return String.format("SendRequest [format=%s, timestampedNodes=%s]", format, timestampedNodes); } @Override @@ -114,7 +113,7 @@ public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((format == null) ? 0 : format.hashCode()); - result = prime * result + ((nodes == null) ? 0 : nodes.hashCode()); + result = prime * result + ((timestampedNodes == null) ? 0 : timestampedNodes.hashCode()); return result; } @@ -132,10 +131,10 @@ public boolean equals(Object obj) { return false; } else if (!format.equals(other.format)) return false; - if (nodes == null) { - if (other.nodes != null) + if (timestampedNodes == null) { + if (other.timestampedNodes != null) return false; - } else if (!nodes.equals(other.nodes)) + } else if (!timestampedNodes.equals(other.timestampedNodes)) return false; return true; } diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/node/TimestampedLwM2mNodesTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/node/TimestampedLwM2mNodesTest.java new file mode 100644 index 0000000000..eb40ebec45 --- /dev/null +++ b/leshan-core/src/test/java/org/eclipse/leshan/core/node/TimestampedLwM2mNodesTest.java @@ -0,0 +1,221 @@ +/******************************************************************************* + * 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: + * Orange - Send with multiple-timestamped values + *******************************************************************************/ +package org.eclipse.leshan.core.node; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; + +public class TimestampedLwM2mNodesTest { + + @Test + public void should_getPathNodesMapForTimestamp_pick_specific_timestamp_nodes() { + // given + TimestampedLwM2mNodes tsNodes = getExampleTimestampedLwM2mNodes(); + + // when + Map tsNodesMap = tsNodes.getNodesAt(123L); + + // then + assertNotNull(tsNodesMap); + assertTrue(tsNodesMap.containsKey(new LwM2mPath("/0/0/1"))); + assertEquals(1L, tsNodesMap.get(new LwM2mPath("/0/0/1")).getId()); + } + + @Test + public void should_getPathNodesMapForTimestamp_pick_null_timestamp_nodes() { + // given + TimestampedLwM2mNodes tsNodes = getExampleMixedTimestampLwM2mNodes(); + + // when + Map tsNodesMap = tsNodes.getNodesAt(null); + + // then + assertNotNull(tsNodesMap); + assertTrue(tsNodesMap.containsKey(new LwM2mPath("/0/0/2"))); + assertEquals(2L, tsNodesMap.get(new LwM2mPath("/0/0/2")).getId()); + } + + @Test + public void should_getPathNodesMapForTimestamp_returns_null_for_nonexistent_timestamp() { + // given + TimestampedLwM2mNodes tsNodes = getExampleTimestampedLwM2mNodes(); + + // when + Map tsNodesMap = tsNodes.getNodesAt(0L); + + // then + assertNull(tsNodesMap); + } + + @Test + public void should_getNodes_returns_all_nodes_ignoring_timestamp() { + // given + TimestampedLwM2mNodes tsNodes = getExampleTimestampedLwM2mNodes(); + + // when + Map tsNodesMap = tsNodes.getNodes(); + + // then + assertNotNull(tsNodesMap); + assertTrue(tsNodesMap.containsKey(new LwM2mPath("/0/0/1"))); + assertTrue(tsNodesMap.containsKey(new LwM2mPath("/0/0/2"))); + assertEquals(1L, tsNodesMap.get(new LwM2mPath("/0/0/1")).getId()); + assertEquals(2L, tsNodesMap.get(new LwM2mPath("/0/0/2")).getId()); + } + + @Test + public void should_getNodes_returns_latest_node_if_path_conflict() { + // given + TimestampedLwM2mNodes tsNodes = getSamePathTimestampedLwM2mNodes(); + + // when + Map tsNodesMap = tsNodes.getNodes(); + + // then + assertNotNull(tsNodesMap); + assertTrue(tsNodesMap.containsKey(new LwM2mPath("/0/0/1"))); + assertEquals(222L, ((LwM2mSingleResource) tsNodesMap.get(new LwM2mPath("/0/0/1"))).getValue()); + } + + @Test + public void should_getTimestamps_returns_ascending_ordered_nodes() { + // given + TimestampedLwM2mNodes tsNodes = getExampleTimestampedLwM2mNodes(); + + // when + Set timestamps = tsNodes.getTimestamps(); + + // then + assertNotNull(timestamps); + Iterator iterator = timestamps.iterator(); + assertEquals(123L, iterator.next().longValue()); + assertEquals(456L, iterator.next().longValue()); + } + + @Test + public void should_null_timestamp_be_considered_as_latest_for_getTimestamps() { + // given + TimestampedLwM2mNodes tsNodes = getExampleMixedTimestampLwM2mNodes(); + + // when + Set timestamps = tsNodes.getTimestamps(); + + // then + assertNotNull(timestamps); + Iterator iterator = timestamps.iterator(); + assertEquals(123L, iterator.next().longValue()); + assertNull(iterator.next()); + } + + @Test + public void should_getNodes_returns_empty_map_for_empty_TimestampedLwM2mNodes() { + // given + TimestampedLwM2mNodes tsNodes = TimestampedLwM2mNodes.builder().build(); + + // when + Map tsNodesMap = tsNodes.getNodes(); + + // then + assertNotNull(tsNodesMap); + assertTrue(tsNodesMap.isEmpty()); + } + + @Test + public void should_getTimestamps_returns_all_timestamps() { + // given + TimestampedLwM2mNodes tsNodes = getExampleTimestampedLwM2mNodes(); + + // when + Set timestamps = tsNodes.getTimestamps(); + + // then + assertNotNull(timestamps); + assertEquals(new HashSet<>(Arrays.asList(123L, 456L)), timestamps); + } + + @Test + public void should_raise_exception_for_duplicates() { + // given + TimestampedLwM2mNodes.Builder builder = TimestampedLwM2mNodes.builder(); + builder.put(456L, new LwM2mPath("/0/0/2"), LwM2mSingleResource.newIntegerResource(2, 222L)); + builder.put(123L, new LwM2mPath("/0/0/1"), LwM2mSingleResource.newIntegerResource(1, 111L)); + builder.put(123L, new LwM2mPath("/0/0/1"), LwM2mSingleResource.newIntegerResource(1, 112L)); + + // when + assertThrows(IllegalArgumentException.class, () -> builder.build()); + } + + @Test + public void should_raise_exception_id_path_does_not_match_node() { + TimestampedLwM2mNodes.Builder builder = TimestampedLwM2mNodes.builder().put(456L, new LwM2mPath("/0/0/2"), + LwM2mResourceInstance.newIntegerInstance(0, 222L)); + assertThrows(IllegalArgumentException.class, () -> builder.build()); + } + + @Test + public void should_raise_exception_path_does_not_match_id() { + TimestampedLwM2mNodes.Builder builder = TimestampedLwM2mNodes.builder().put(456L, new LwM2mPath("/0/0/2"), + LwM2mSingleResource.newIntegerResource(1, 222L)); + assertThrows(IllegalArgumentException.class, () -> builder.build()); + } + + @Test + public void should_not_raise_exception_for_duplicates() { + // given + TimestampedLwM2mNodes.Builder builder = TimestampedLwM2mNodes.builder().raiseExceptionOnDuplicate(false); + builder.put(456L, new LwM2mPath("/0/0/2"), LwM2mSingleResource.newIntegerResource(2, 222L)); + builder.put(123L, new LwM2mPath("/0/0/1"), LwM2mSingleResource.newIntegerResource(1, 111L)); + builder.put(123L, new LwM2mPath("/0/0/1"), LwM2mSingleResource.newIntegerResource(1, 112L)); + + // when + TimestampedLwM2mNodes nodes = builder.build(); + + // then + assertNotNull(nodes); + TimestampedLwM2mNodes.Builder expected = TimestampedLwM2mNodes.builder(); + expected.put(456L, new LwM2mPath("/0/0/2"), LwM2mSingleResource.newIntegerResource(2, 222L)); + expected.put(123L, new LwM2mPath("/0/0/1"), LwM2mSingleResource.newIntegerResource(1, 112L)); + assertEquals(expected.build(), nodes); + } + + private TimestampedLwM2mNodes getExampleTimestampedLwM2mNodes() { + TimestampedLwM2mNodes.Builder tsNodes = TimestampedLwM2mNodes.builder(); + tsNodes.put(456L, new LwM2mPath("/0/0/2"), LwM2mSingleResource.newIntegerResource(2, 222L)); + tsNodes.put(123L, new LwM2mPath("/0/0/1"), LwM2mSingleResource.newIntegerResource(1, 111L)); + return tsNodes.build(); + } + + private TimestampedLwM2mNodes getSamePathTimestampedLwM2mNodes() { + TimestampedLwM2mNodes.Builder tsNodes = TimestampedLwM2mNodes.builder(); + tsNodes.put(456L, new LwM2mPath("/0/0/1"), LwM2mSingleResource.newIntegerResource(1, 222L)); + tsNodes.put(123L, new LwM2mPath("/0/0/1"), LwM2mSingleResource.newIntegerResource(1, 111L)); + return tsNodes.build(); + } + + private TimestampedLwM2mNodes getExampleMixedTimestampLwM2mNodes() { + TimestampedLwM2mNodes.Builder tsNodes = TimestampedLwM2mNodes.builder(); + tsNodes.put(new LwM2mPath("/0/0/2"), LwM2mSingleResource.newIntegerResource(2, 222L)); + tsNodes.put(123L, new LwM2mPath("/0/0/1"), LwM2mSingleResource.newIntegerResource(1, 111L)); + return tsNodes.build(); + } +} \ No newline at end of file diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java index 4e47e396cb..02ecc3e853 100644 --- a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java +++ b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java @@ -40,9 +40,11 @@ import org.eclipse.leshan.core.node.LwM2mObjectInstance; import org.eclipse.leshan.core.node.LwM2mPath; 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.node.ObjectLink; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.tlv.Tlv; import org.eclipse.leshan.core.tlv.Tlv.TlvType; @@ -1259,4 +1261,28 @@ public void senml_cbor_empty_multi_resource() { assertEquals(6, resource.getId()); assertTrue(resource.getInstances().isEmpty()); } + + @Test + public void senml_multiple_timestamped_nodes() throws CodecException { + // given + StringBuilder b = new StringBuilder(); + b.append("[{\"bn\":\"/4/0/\",\"bt\":268600000,\"n\":\"0\",\"v\":1,\"t\":1},"); + b.append("{\"n\":\"1\",\"v\":2,\"t\":2},"); + b.append("{\"n\":\"1\",\"v\":3,\"t\":3},"); + b.append("{\"bn\":\"/3/0/7/\",\"n\":\"0\",\"v\":3800}"); + b.append("]"); + + // when + TimestampedLwM2mNodes data = decoder.decodeTimestampedNodes(b.toString().getBytes(), ContentFormat.SENML_JSON, + model); + + // then + TimestampedLwM2mNodes.Builder expectedResult = new TimestampedLwM2mNodes.Builder() + .put(268600000L, new LwM2mPath("/3/0/7/0"), LwM2mResourceInstance.newIntegerInstance(0, 3800)) + .put(268600001L, new LwM2mPath("/4/0/0"), LwM2mSingleResource.newIntegerResource(0, 1)) + .put(268600002L, new LwM2mPath("/4/0/1"), LwM2mSingleResource.newIntegerResource(1, 2)) + .put(268600003L, new LwM2mPath("/4/0/1"), LwM2mSingleResource.newIntegerResource(1, 3)); + + assertEquals(expectedResult.build(), data); + } } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTest.java index 2dbcd34e61..32981c9baf 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTest.java @@ -27,6 +27,7 @@ import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.model.StaticModel; import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.response.SendResponse; @@ -95,12 +96,12 @@ public void can_send_resources() throws InterruptedException, TimeoutException { // wait for data and check result listener.waitForData(1, TimeUnit.SECONDS); assertNotNull(listener.getRegistration()); - Map data = listener.getData(); - LwM2mResource modelnumber = (LwM2mResource) data.get("/3/0/1"); + Map data = listener.getNodes(); + LwM2mResource modelnumber = (LwM2mResource) data.get(new LwM2mPath("/3/0/1")); assertEquals(modelnumber.getId(), 1); assertEquals(modelnumber.getValue(), "IT-TEST-123"); - LwM2mResource serialnumber = (LwM2mResource) data.get("/3/0/2"); + LwM2mResource serialnumber = (LwM2mResource) data.get(new LwM2mPath("/3/0/2")); assertEquals(serialnumber.getId(), 2); assertEquals(serialnumber.getValue(), "12345"); } @@ -122,12 +123,12 @@ public void can_send_resources_asynchronously() throws InterruptedException, Tim // wait for data and check result listener.waitForData(1, TimeUnit.SECONDS); assertNotNull(listener.getRegistration()); - Map data = listener.getData(); - LwM2mResource modelnumber = (LwM2mResource) data.get("/3/0/1"); + Map data = listener.getNodes(); + LwM2mResource modelnumber = (LwM2mResource) data.get(new LwM2mPath("/3/0/1")); assertEquals(modelnumber.getId(), 1); assertEquals(modelnumber.getValue(), "IT-TEST-123"); - LwM2mResource serialnumber = (LwM2mResource) data.get("/3/0/2"); + LwM2mResource serialnumber = (LwM2mResource) data.get(new LwM2mPath("/3/0/2")); assertEquals(serialnumber.getId(), 2); assertEquals(serialnumber.getValue(), "12345"); } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTimestampedTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTimestampedTest.java new file mode 100644 index 0000000000..c8ddd3fd37 --- /dev/null +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/send/SendTimestampedTest.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * 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: + * Orange - Send with multiple-timestamped values + *******************************************************************************/ +package org.eclipse.leshan.integration.tests.send; + +import static org.junit.Assert.*; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.leshan.client.californium.LeshanClientBuilder; +import org.eclipse.leshan.client.object.Security; +import org.eclipse.leshan.client.object.Server; +import org.eclipse.leshan.client.resource.DummyInstanceEnabler; +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.resource.ObjectsInitializer; +import org.eclipse.leshan.client.resource.SimpleInstanceEnabler; +import org.eclipse.leshan.client.servers.ServerIdentity; +import org.eclipse.leshan.core.LwM2mId; +import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.model.StaticModel; +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.TimestampedLwM2mNodes; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.integration.tests.util.IntegrationTestHelper; +import org.eclipse.leshan.integration.tests.util.SynchronousSendListener; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class SendTimestampedTest { + + protected final IntegrationTestHelper helper = new TestHelperWithFakeDecoder(); + + @Before + public void start() { + helper.initialize(); + helper.createServer(); + helper.server.start(); + helper.createClient(); + helper.client.start(); + helper.waitForRegistrationAtServerSide(1); + } + + @After + public void stop() { + helper.client.destroy(false); + helper.server.destroy(); + helper.dispose(); + } + + @Test + public void server_handle_multiple_timestamped_node() throws InterruptedException, TimeoutException { + // Define send listener + SynchronousSendListener listener = new SynchronousSendListener(); + helper.server.getSendService().addListener(listener); + + // Send Data + helper.waitForRegistrationAtClientSide(1); + ServerIdentity server = helper.client.getRegisteredServers().values().iterator().next(); + helper.client.sendData(server, ContentFormat.SENML_JSON, Arrays.asList(getExamplePath().toString()), 1000); + listener.waitForData(1, TimeUnit.SECONDS); + + // Verify SendListener data received + assertNotNull(listener.getRegistration()); + TimestampedLwM2mNodes data = listener.getData(); + + Map exampleNodes = getExampleTimestampedNodes(); + assertEquals(exampleNodes.keySet(), data.getTimestamps()); + + for (Long ts : exampleNodes.keySet()) { + Map pathNodeMap = data.getNodesAt(ts); + assertTrue(pathNodeMap.containsKey(getExamplePath())); + + LwM2mNode node = pathNodeMap.get(getExamplePath()); + LwM2mNode expectedNode = exampleNodes.get(ts); + + assertEquals(node, expectedNode); + } + } + + private static LwM2mPath getExamplePath() { + return new LwM2mPath("/2000/1/3"); + } + + private static Map getExampleTimestampedNodes() { + Map timestampedNodes = new HashMap<>(); + timestampedNodes.put(268435456L, LwM2mSingleResource.newFloatResource(3, 12345)); + timestampedNodes.put(268435457L, LwM2mSingleResource.newFloatResource(3, 67890)); + return timestampedNodes; + } + + private static class TestHelperWithFakeDecoder extends IntegrationTestHelper { + @Override + protected ObjectsInitializer createObjectsInitializer() { + return new ObjectsInitializer(new StaticModel(createObjectModels())); + } + + @Override + public void createClient(Map additionalAttributes) { + // Create objects Enabler + ObjectsInitializer initializer = createObjectsInitializer(); + initializer.setInstancesForObject(LwM2mId.SECURITY, + Security.noSec("coap://" + server.getUnsecuredAddress().getHostString() + ":" + + server.getUnsecuredAddress().getPort(), 12345)); + initializer.setInstancesForObject(LwM2mId.SERVER, new Server(12345, LIFETIME)); + initializer.setInstancesForObject(LwM2mId.DEVICE, new TestDevice("Eclipse Leshan", MODEL_NUMBER, "12345")); + initializer.setClassForObject(LwM2mId.ACCESS_CONTROL, DummyInstanceEnabler.class); + initializer.setInstancesForObject(TEST_OBJECT_ID, new DummyInstanceEnabler(0), + new SimpleInstanceEnabler(1, FLOAT_RESOURCE_ID, 12345d)); + List objects = initializer.createAll(); + + // Build Client + LeshanClientBuilder builder = new LeshanClientBuilder(currentEndpointIdentifier.get()); + builder.setDecoder(new DefaultLwM2mDecoder(true)); + builder.setEncoder(new FakeEncoder()); + builder.setAdditionalAttributes(additionalAttributes); + builder.setObjects(objects); + client = builder.build(); + setupClientMonitoring(); + } + } + + private static class FakeEncoder extends DefaultLwM2mEncoder { + public FakeEncoder() { + super(true); + } + + @Override + public byte[] encodeNodes(Map nodes, ContentFormat format, LwM2mModel model) { + return ("[{\"bn\":\"/2000/1/\",\"n\":\"3\",\"v\":12345,\"t\":268435456}," + + "{\"n\":\"3\",\"v\":67890,\"t\":268435457}]").getBytes(StandardCharsets.UTF_8); + } + } +} diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousSendListener.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousSendListener.java index 52a9ad0cb2..436f09a10f 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousSendListener.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousSendListener.java @@ -21,13 +21,15 @@ import java.util.concurrent.TimeoutException; import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.request.SendRequest; import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.send.SendListener; public class SynchronousSendListener implements SendListener { private CountDownLatch dataLatch = new CountDownLatch(1); - private volatile Map data; + private volatile TimestampedLwM2mNodes data; private CountDownLatch errorLatch = new CountDownLatch(1); private volatile Exception error; @@ -35,7 +37,7 @@ public class SynchronousSendListener implements SendListener { private volatile Registration registration; @Override - public void dataReceived(Registration registration, Map data, SendRequest request) { + public void dataReceived(Registration registration, TimestampedLwM2mNodes data, SendRequest request) { this.data = data; this.registration = registration; dataLatch.countDown(); @@ -48,10 +50,14 @@ public void onError(Registration registration, Exception error) { errorLatch.countDown(); } - public Map getData() { + public TimestampedLwM2mNodes getData() { return data; } + public Map getNodes() { + return data.getNodes(); + } + public Registration getRegistration() { return registration; } diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/send/SendResource.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/send/SendResource.java index db5d5c6eb4..502555a46b 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/send/SendResource.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/send/SendResource.java @@ -17,16 +17,12 @@ import static org.eclipse.leshan.core.californium.ResponseCodeUtil.toCoapResponseCode; -import java.util.List; -import java.util.Map; - import org.eclipse.californium.core.coap.CoAP.ResponseCode; import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.leshan.core.californium.LwM2mCoapResource; 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.TimestampedLwM2mNodes; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.node.codec.LwM2mDecoder; import org.eclipse.leshan.core.request.ContentFormat; @@ -83,9 +79,8 @@ public void handlePOST(CoapExchange exchange) { "Unsupported content format [%s] in [%s] from [%s]", contentFormat, coapRequest, sender)); return; } - Map data = null; - data = decoder.decodeNodes(payload, contentFormat, (List) null, model); + TimestampedLwM2mNodes data = decoder.decodeTimestampedNodes(payload, contentFormat, model); // Handle "send op request SendRequest sendRequest = new SendRequest(contentFormat, data, coapRequest); 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 index dd6304b035..a428d34a5c 100644 --- 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 @@ -25,6 +25,7 @@ import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.node.codec.LwM2mDecoder; import org.eclipse.leshan.core.request.ContentFormat; @@ -54,6 +55,12 @@ public List decodeTimestampedData(byte[] content, ContentF return Collections.singletonList(new TimestampedLwM2mNode(null, decode(null, null, null, null))); } + @Override + public TimestampedLwM2mNodes decodeTimestampedNodes(byte[] content, ContentFormat format, LwM2mModel model) + throws CodecException { + return null; + } + @Override public List decodePaths(byte[] content, ContentFormat format) throws CodecException { return null; diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendHandler.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendHandler.java index bb096e5b49..b40c9b917b 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendHandler.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendHandler.java @@ -15,15 +15,10 @@ *******************************************************************************/ package org.eclipse.leshan.server.send; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import java.util.concurrent.CopyOnWriteArrayList; -import org.eclipse.leshan.core.node.LwM2mNode; -import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.request.SendRequest; import org.eclipse.leshan.core.response.SendResponse; import org.eclipse.leshan.core.response.SendableResponse; @@ -52,20 +47,15 @@ public SendableResponse handleSend(final Registration registration SendableResponse response = new SendableResponse<>(SendResponse.success(), new Runnable() { @Override public void run() { - fireDataReceived(registration, request.getNodes(), request); + fireDataReceived(registration, request.getTimestampedNodes(), request); } }); return response; } - protected void fireDataReceived(Registration registration, Map data, SendRequest request) { - HashMap nodes = new HashMap<>(); - for (Entry entry : data.entrySet()) { - nodes.put(entry.getKey().toString(), entry.getValue()); - } - + protected void fireDataReceived(Registration registration, TimestampedLwM2mNodes data, SendRequest request) { for (SendListener listener : listeners) { - listener.dataReceived(registration, Collections.unmodifiableMap(nodes), request); + listener.dataReceived(registration, data, request); } } diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendListener.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendListener.java index a1db5922b1..0f3b673b14 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendListener.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/send/SendListener.java @@ -15,9 +15,7 @@ *******************************************************************************/ package org.eclipse.leshan.server.send; -import java.util.Map; - -import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.request.SendRequest; import org.eclipse.leshan.server.registration.Registration; @@ -35,7 +33,7 @@ public interface SendListener { * @param data The data received * @param request The request received */ - void dataReceived(Registration registration, Map data, SendRequest request); + void dataReceived(Registration registration, TimestampedLwM2mNodes data, SendRequest request); /** * Called when Send Request can't not be handled by server (because of e.g. unsupported content format or invalid 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 dd2fd2c8c1..e0d2d5f4b3 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 @@ -22,7 +22,6 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -34,6 +33,7 @@ import org.eclipse.leshan.core.link.Link; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.TimestampedLwM2mNodes; import org.eclipse.leshan.core.observation.CompositeObservation; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.observation.SingleObservation; @@ -238,7 +238,7 @@ public void newObservation(Observation observation, Registration registration) { private final SendListener sendListener = new SendListener() { @Override - public void dataReceived(Registration registration, Map data, SendRequest request) { + public void dataReceived(Registration registration, TimestampedLwM2mNodes data, SendRequest request) { if (LOG.isDebugEnabled()) { LOG.debug("Received Send request from [{}] containing value [{}]", registration, data.toString()); @@ -246,7 +246,7 @@ public void dataReceived(Registration registration, Map data, if (registration != null) { try { - String jsonContent = EventServlet.this.mapper.writeValueAsString(data); + String jsonContent = EventServlet.this.mapper.writeValueAsString(data.getNodes()); String eventData = new StringBuilder("{\"ep\":\"") // .append(registration.getEndpoint()) //