Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Timestamped Nodes support to Send Operation at Server side. #1218

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
291c926
Add Timestamped nodes support to Send Operation at server side.
Mar 2, 2022
f46fc9f
Fix javadoc issue : no description for @param
sbernard31 Mar 10, 2022
20f189b
Fix Eclipse Warnings.
sbernard31 Mar 10, 2022
19b1831
Fix some formatting issue
sbernard31 Mar 10, 2022
1fe56a7
SendRequest : remove duplicate code.
sbernard31 Mar 10, 2022
2e95bb9
Removed unused TimestampedInstanceEnabler.
sbernard31 Mar 10, 2022
4b89dd5
Revert formatting as it seems there is no other changes in this file
sbernard31 Mar 10, 2022
aa6817e
add some check to Default Decoder/Encoder
sbernard31 Mar 10, 2022
b15ec7f
Fix retro comptability in EventServlet to not break the demo.
sbernard31 Mar 10, 2022
ae0073f
Rename : remove "Multi" in (encode/decode)MultiTimestampedNodes.
sbernard31 Mar 10, 2022
2249f45
Rename getNodesForTimestamp in getNodesAt
sbernard31 Mar 10, 2022
758fd57
Fix LwM2mNodeDecoderTest.senml_multiple_timestamped_nodes
sbernard31 Mar 16, 2022
278ee9b
Make TimestampedLwM2mNodes really immutable.
sbernard31 Mar 16, 2022
d346379
Add some validation.
sbernard31 Mar 16, 2022
a0f18ad
Use fluent notation in senml_multiple_timestamped_nodes test
sbernard31 Mar 17, 2022
65be110
Fix test name and remove duplicate tests in TimestampedLwM2mNodesTest
sbernard31 Mar 17, 2022
343a97f
Fix TimestampedLwM2mNodes.toString()
sbernard31 Mar 17, 2022
5d01cec
Fix LwM2mNodeDecoderTest.senml_multiple_timestamped_nodes test
sbernard31 Mar 17, 2022
74b3eb1
Add some path/node consistency validation in TimestampedLwM2mNodes
sbernard31 Mar 17, 2022
9f69122
Remove unnecessary empty line TimestampedLwM2mNodesTest
sbernard31 Mar 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Long, Map<LwM2mPath, LwM2mNode>> timestampedPathNodesMap;

private TimestampedLwM2mNodes(Map<Long, Map<LwM2mPath, LwM2mNode>> 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<LwM2mPath, LwM2mNode> getNodesAt(Long timestamp) {
Map<LwM2mPath, LwM2mNode> 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<LwM2mPath, LwM2mNode> getNodes() {
Map<LwM2mPath, LwM2mNode> result = new HashMap<>();
for (Map.Entry<Long, Map<LwM2mPath, LwM2mNode>> 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<Long> 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<InternalNode> nodes = new ArrayList<>();
private boolean noDuplicate = true;

public Builder raiseExceptionOnDuplicate(boolean raiseException) {
noDuplicate = raiseException;
return this;
}

public Builder addNodes(Map<LwM2mPath, LwM2mNode> pathNodesMap) {
for (Entry<LwM2mPath, LwM2mNode> 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<LwM2mPath, LwM2mNode> pathNodeMap = timestampedNodes.getNodesAt(timestamp);
for (Map.Entry<LwM2mPath, LwM2mNode> 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<Long, Map<LwM2mPath, LwM2mNode>> 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<LwM2mPath, LwM2mNode> 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<Long> getTimestampComparator() {
return (o1, o2) -> {
if (o1 == null) {
return (o2 == null) ? 0 : 1;
} else if (o2 == null) {
return -1;
} else {
return o1.compareTo(o2);
}
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -204,6 +205,31 @@ public List<TimestampedLwM2mNode> 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<LwM2mPath> decodePaths(byte[] content, ContentFormat format) throws CodecException {
LOG.trace("Decoding paths encoded with {}: {}", format, content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -199,6 +200,23 @@ public byte[] encodeTimestampedData(List<TimestampedLwM2mNode> 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<LwM2mPath> paths, ContentFormat format) throws CodecException {
Validate.notEmpty(paths);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -97,6 +98,19 @@ Map<LwM2mPath, LwM2mNode> decodeNodes(byte[] content, ContentFormat format, List
List<TimestampedLwM2mNode> decodeTimestampedData(byte[] content, ContentFormat format, LwM2mPath path,
LwM2mModel model) throws CodecException;

/**
* Deserializes a binary content into a {@link TimestampedLwM2mNodes}.
* <p>
*
* @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;

sbernard31 marked this conversation as resolved.
Show resolved Hide resolved
/**
* Deserializes a binary content into a list of {@link LwM2mPath}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -73,6 +74,19 @@ public interface LwM2mEncoder {
byte[] encodeTimestampedData(List<TimestampedLwM2mNode> 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.
*
Expand Down
Loading