Skip to content

Commit

Permalink
#1218: Add Timestamped nodes support to Send Operation at server side.
Browse files Browse the repository at this point in the history
Signed-off-by: Michał Wadowski <Michal.Wadowski@orange.com>
Also-by: Simon Bernard <sbernard@sierrawireless.com>
  • Loading branch information
Michał Wadowski authored and sbernard31 committed Mar 18, 2022
1 parent 21bf6b9 commit 633e3e2
Show file tree
Hide file tree
Showing 20 changed files with 835 additions and 57 deletions.
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;

/**
* 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

0 comments on commit 633e3e2

Please sign in to comment.